Skip to main content
Кэш запросов позволяет выполнять SELECT-запросы только один раз, а при последующих выполнениях того же запроса отдавать результаты напрямую из кэша. В зависимости от типа запросов это может значительно снизить задержку и расход ресурсов ClickHouse server.

Предпосылки, устройство и ограничения

Кэши запросов в целом можно разделить на транзакционно согласованные и транзакционно несогласованные.
  • В транзакционно согласованных кэшах база данных инвалидирует (отбрасывает) кэшированные результаты запросов, если результат запроса SELECT изменяется или потенциально может измениться. В ClickHouse к операциям, изменяющим данные, относятся вставки/обновления/удаления в таблицах или из таблиц, а также схлопывающие слияния. Транзакционно согласованное кэширование особенно хорошо подходит для OLTP-баз данных, например MySQL (где кэш запросов был удалён после v8.0) и Oracle.
  • В транзакционно несогласованных кэшах допускаются небольшие неточности в результатах запросов при условии, что всем записям кэша назначается срок действия, по истечении которого они устаревают (например, 1 минута), и что базовые данные в течение этого периода изменяются лишь незначительно. Такой подход в целом лучше подходит для OLAP-баз данных. В качестве примера, где транзакционно несогласованного кэширования достаточно, рассмотрим почасовой отчёт по продажам в инструменте отчётности, который одновременно просматривают несколько пользователей. Данные о продажах обычно изменяются достаточно медленно, поэтому базе данных нужно вычислить отчёт только один раз (это соответствует первому запросу SELECT). Последующие запросы могут обслуживаться напрямую из кэша запросов. В этом примере разумный срок действия может составлять 30 мин.
Транзакционно несогласованное кэширование традиционно обеспечивается клиентскими инструментами или прокси-пакетами (например, chproxy), взаимодействующими с базой данных. В результате одна и та же логика кэширования и конфигурация часто дублируются. Благодаря кэшу запросов ClickHouse логика кэширования переносится на сторону сервера. Это сокращает затраты на сопровождение и позволяет избежать избыточности.

Настройки конфигурации и использование

В ClickHouse Cloud для изменения настроек кэша запросов необходимо использовать настройки на уровне запроса. Изменение настроек на уровне конфигурации в настоящее время не поддерживается.
clickhouse-local выполняет только один запрос за раз. Поскольку кэширование результатов запросов в памяти не имеет смысла для одного процесса, кэш запросов в памяти в clickhouse-local отключён. При этом доступен кэш запросов на диске: его записи хранятся в файловом кэше на диске, поэтому результаты, закэшированные одним процессом clickhouse-local, могут использоваться последующими процессами, работающими с тем же каталогом файлового кэша.
Параметр use_query_cache можно использовать, чтобы указать, должен ли конкретный запрос или все запросы текущего сеанса использовать кэш запросов. Например, при первом выполнении запроса
сохранит результат запроса в кэше запросов. При последующих выполнениях того же запроса (также с параметром use_query_cache = true) будет считываться вычисленный результат из кэша и немедленно возвращаться.
Параметр use_query_cache и все остальные настройки, связанные с кэшем запросов, действуют только для отдельных операторов SELECT. В частности, результаты SELECT к представлениям, созданным с помощью CREATE VIEW AS SELECT [...] SETTINGS use_query_cache = true, не кэшируются, если только сам оператор SELECT не выполняется с SETTINGS use_query_cache = true.
Способ использования кэша можно более тонко настроить с помощью параметров enable_writes_to_query_cache и enable_reads_from_query_cache (оба по умолчанию имеют значение true). Первый параметр управляет тем, сохраняются ли результаты запросов в кэше, а второй определяет, должна ли база данных пытаться получать результаты запросов из кэша. Например, следующий запрос будет использовать кэш только пассивно, то есть пытаться читать из него, но не сохранять в нём свой результат:
Для максимального контроля обычно рекомендуется указывать настройки use_query_cache, enable_writes_to_query_cache и enable_reads_from_query_cache только для конкретных запросов. Также можно включить кэширование на уровне пользователя или профиля (например, через SET use_query_cache = true), однако следует помнить, что тогда все запросы SELECT могут возвращать кэшированные результаты. Кэш запросов можно очистить с помощью оператора SYSTEM CLEAR QUERY CACHE. Содержимое кэша запросов отображается в системной таблице system.query_cache. Число попаданий и промахов кэша запросов с момента запуска базы данных показано как события “QueryCacheHits” и “QueryCacheMisses” в системной таблице system.events. Оба счётчика обновляются только для запросов SELECT, выполняемых с настройкой use_query_cache = true; другие запросы не влияют на “QueryCacheMisses”. Счётчики описывают кэш запросов в целом: если включён кэш запросов на диске (см. ниже), запрос, результат которого не найден в памяти, но затем получен с диска, учитывается как одно событие “QueryCacheHits”, а запрос, результат которого не найден ни в одном из хранилищ, — как одно событие “QueryCacheMisses”. Поле query_cache_usage в системной таблице system.query_log показывает для каждого выполненного запроса, был ли результат запроса записан в кэш запросов или прочитан из него. Метрики QueryCacheEntries и QueryCacheBytes в системной таблице system.metrics показывают, сколько записей / байт сейчас содержит кэш запросов в памяти. Они не учитывают записи кэша запросов на диске, для которого пока нет метрик заполненности. Кэш запросов существует в единственном экземпляре для каждого процесса сервера ClickHouse. Однако кэшированные результаты по умолчанию не являются общими для разных пользователей. Это можно изменить (см. ниже), но по соображениям безопасности делать этого не рекомендуется. Результаты запросов хранятся в кэше запросов с привязкой к абстрактному синтаксическому дереву (AST) соответствующего запроса. Это означает, что кэширование не зависит от регистра: например, SELECT 1 и select 1 считаются одним и тем же запросом. Чтобы сопоставление было более естественным, из AST удаляются все настройки уровня запроса, связанные с кэшем запросов и форматированием вывода). Если выполнение запроса было прервано из-за исключения или отменено пользователем, запись в кэш запросов не добавляется. Размер кэша запросов в байтах, максимальное количество записей в кэше и максимальный размер отдельной записи (в байтах и в строках) можно настроить с помощью различных параметров конфигурации сервера.
Также можно ограничить использование кэша отдельными пользователями с помощью профилей настроек и ограничений на настройки. В частности, можно ограничить максимальный объём памяти (в байтах), который пользователь может выделить под кэш запросов в памяти, а также максимальное количество сохраняемых результатов запросов (эти ограничения не распространяются на кэш запросов на диске). Для этого сначала задайте параметры query_cache_max_size_in_bytes и query_cache_max_entries в профиле пользователя в users.xml, затем сделайте оба параметра доступными только для чтения:
Чтобы задать минимальное время выполнения запроса, при котором его результат можно кэшировать, используйте настройку query_cache_min_query_duration. Например, результат запроса
кэшируется, только если запрос выполняется дольше 5 секунд. Также можно указать, сколько раз должен выполниться запрос, прежде чем его результат будет кэшироваться, — для этого используйте настройку query_cache_min_query_runs. Записи в кэше запросов становятся устаревшими по истечении определенного периода времени (time-to-live). По умолчанию этот период составляет 60 секунд, но можно указать другое значение на уровне сеанса, профиля или запроса с помощью настройки query_cache_ttl. Кэш запросов вытесняет записи “лениво”, то есть когда запись устаревает, она не удаляется из кэша немедленно. Вместо этого, когда в кэш запросов нужно добавить новую запись, база данных проверяет, достаточно ли в кэше свободного места для новой записи. Если это не так, база данных пытается удалить все устаревшие записи. Если в кэше по-прежнему недостаточно свободного места, новая запись не добавляется. Если запрос выполняется через HTTP, ClickHouse устанавливает заголовки Age и Expires, указывая возраст (в секундах) и временную метку истечения кэшированной записи. Записи в кэше запросов по умолчанию сжимаются. Это уменьшает общее потребление памяти ценой более медленной записи в кэш запросов / чтения из кэша запросов. Чтобы отключить сжатие, используйте настройку query_cache_compress_entries. Иногда бывает полезно хранить в кэше несколько результатов для одного и того же запроса. Этого можно добиться с помощью настройки query_cache_tag, которая выступает как метка (или пространство имен) для записей кэша запросов. Кэш запросов считает результаты одного и того же запроса с разными тегами разными. Пример создания трех разных записей кэша запросов для одного и того же запроса:
Чтобы удалить из кэша запросов только записи с тегом tag, можно использовать оператор SYSTEM CLEAR QUERY CACHE TAG 'tag'.

Кэширование подзапросов

По умолчанию use_query_cache для внешнего запроса не распространяется на подзапросы. Это означает, что для каждого подзапроса кэширование нужно явно включить:
В этом примере кэшируется только результат внутреннего подзапроса. Внешний запрос не кэшируется. Чтобы включить кэширование для всех подзапросов сразу, используйте настройку query_cache_for_subqueries:
Чтобы явно отключить кэширование для конкретного подзапроса при включенном массовом применении, установите use_query_cache = false для этого подзапроса:
Записи кэша подзапросов, хранящиеся в кэше запросов в памяти, видны в system.query_cache при is_subquery = 1. Результаты подзапросов также кэшируются на диске, если включен кэш запросов на диске, однако эти записи не отображаются в system.query_cache. Настройка query_cache_ttl также применяется к записям кэша подзапросов и может задаваться для каждого подзапроса. ClickHouse читает данные таблицы блоками по max_block_size строк. Из-за фильтрации, агрегации и т. п. результирующие блоки обычно значительно меньше, чем ‘max_block_size’, хотя в некоторых случаях они могут быть и гораздо больше. Настройка query_cache_squash_partial_results (включена по умолчанию) определяет, будут ли результирующие блоки объединяться (если они слишком малы) или разделяться (если они большие) на блоки размером ‘max_block_size’ перед записью в кэш результатов запросов. Это снижает производительность записи в кэш запросов, но повышает степень сжатия записей кэша и обеспечивает более естественную гранулярность блоков, когда результаты запросов впоследствии отдаются из кэша запросов. В результате кэш запросов хранит для каждого запроса несколько (частичных) результирующих блоков. Хотя такое поведение по умолчанию вполне оправданно, его можно отключить с помощью настройки query_cache_squash_partial_results. Кроме того, результаты запросов с недетерминированными функциями по умолчанию не кэшируются. К таким функциям относятся Чтобы принудительно кэшировать результаты запросов с недетерминированными функциями, используйте настройку query_cache_nondeterministic_function_handling. Результаты запросов, использующих системные таблицы (например, system.processes` или information_schema.tables), по умолчанию не кэшируются. Чтобы всё же принудительно кэшировать результаты запросов к системным таблицам, используйте настройку query_cache_system_table_handling. Наконец, по соображениям безопасности записи в кэше запросов не являются общими для разных пользователей. Например, пользователь A не должен иметь возможность обойти ROW POLICY для таблицы, выполнив тот же запрос, что и другой пользователь B, для которого такая политика не задана. Однако при необходимости записи кэша можно пометить как доступные другим пользователям (то есть общие), указав настройку query_cache_share_between_users.

Кэш запросов на диске

По умолчанию кэш запросов хранит свои записи в памяти. Кроме того, результаты запросов можно кэшировать на диске с использованием файлового кэша. По сравнению с кэшем запросов в памяти кэш запросов на диске вмещает больше данных и сохраняется после перезапуска сервера. Запись может быть использована только той же сборкой сервера, которая её создала: после обновления или понижения версии обращения к записям, созданным предыдущей сборкой, приводят к промахам кэша, а сами записи со временем перезаписываются или вытесняются. Чтобы использовать кэш запросов на диске, сначала настройте файловый кэш в разделе filesystem_caches конфигурации сервера:
затем укажите его имя в настройке query_cache_on_disk_cache_name:
Кэш запросов на диске работает независимо от кэша запросов в памяти, и для него можно отдельно включить чтение и/или запись с помощью настроек enable_reads_from_query_cache_on_disk и enable_writes_to_query_cache_on_disk (обе по умолчанию имеют значение true ). Если чтение включено для обоих кэшей, поиск сначала выполняется в памяти и только при промахе — на диске. Если запись включена для обоих кэшей, результат сохраняется в оба. Если ни один кэш не может сохранить результат (например, в clickhouse-local, где нет кэша запросов в памяти, при enable_writes_to_query_cache_on_disk = false), запрос не отклоняется из-за недетерминированных функций, системных таблиц или режима переполнения, отличного от throw: эти проверки лишь защищают от сохранения неверных результатов. Записи на диске сжимаются кодеком, заданным настройкой query_cache_on_disk_codec (по умолчанию ZSTD(3)). Настройки сервера max_entry_size_in_bytes и max_entry_size_in_rows кэша запросов ограничивают записи в обоих хранилищах; каждое хранилище измеряет запись в том виде, в котором её хранит, поэтому на диске ограничение в байтах применяется к сериализованной сжатой записи. Если любой из них присвоить значение 0, запись в оба хранилища отключается. Все остальные настройки кэша запросов (например, query_cache_ttl, query_cache_min_query_duration) действуют на кэш запросов на диске так же, как и на кэш запросов в памяти, за двумя исключениями, описанными ниже. Настройки query_cache_max_size_in_bytes и query_cache_max_entries задают ограничения для каждого пользователя только в кэше запросов в памяти. На кэш запросов на диске они не распространяются: объём записей на диске ограничен размером настроенного файлового кэша, а вытесняются они по его правилам (см. ниже), поэтому пользователь со строгим профилем настроек всё равно может сохранить на диске больше результатов, чем эти настройки допускают в памяти. Это сделано намеренно: файловый кэш — общий ресурс ограниченного размера, и кэш запросов на диске не резервирует в нём место. Настройка query_cache_share_between_users — ещё одно исключение: для обоих хранилищ она означает одно и то же (запись, сделанная с ней, выдаётся всем пользователям), однако неразделяемые записи изолируются по-разному. Кэш запросов в памяти хранит одну запись на запрос и отклоняет её при чтении, если она принадлежит другому пользователю, поэтому неразделяемая запись пользователя A блокирует запись результатов пользователя B, пока срок её действия не истечёт или она не будет вытеснена. Кэш запросов на диске, напротив, включает пользователя и набор его текущих ролей в ключ неразделяемой записи, поэтому у пользователя B происходит промах по записи пользователя A, и он сохраняет собственную копию результата. То же относится к одному пользователю с разными наборами текущих ролей, например после SET ROLE: роли определяют привилегии и ROW POLICY, поэтому результаты, вычисленные под разными ролями, кэшируются раздельно. Записи кэша запросов на диске являются обычными записями файлового кэша и ничем не отличаются от других данных, кэшированных в том же файловом кэше: они не защищены от удаления и вытесняются по тем же правилам (например, least-recently-used), когда в файловом кэше заканчивается место. Отдельных ограничений на их общий размер или количество нет — действуют только ограничения самого файлового кэша. Оператор SYSTEM CLEAR QUERY CACHE (и SYSTEM CLEAR QUERY CACHE TAG 'tag') также удаляет записи кэша запросов на диске (с указанным тегом; повреждённые записи и записи несовместимого формата удаляются независимо от тега) из файлового кэша, выбранного настройкой сеанса query_cache_on_disk_cache_name (например, после SET query_cache_on_disk_cache_name = 'query_results'), не затрагивая остальные данные файлового кэша. Чтобы найти эти записи, он обходит метаданные всех сегментов файлов файлового кэша (в памяти, без чтения с диска) и считывает заголовок каждого ключа, помеченного как запись кэша запросов. Время обхода пропорционально количеству сегментов файлов в файловом кэше, поэтому если выделить файловый кэш исключительно под результаты запросов, SYSTEM CLEAR QUERY CACHE будет выполняться быстро. Если файловый кэш уже вытеснил начало записи, оставшаяся часть записи сохраняется, пока файловый кэш не вытеснит её или пока тот же результат запроса не будет записан заново, однако для ответов она никогда не используется. Запись, которая ещё находится в процессе записи, также сохраняется. Оператор SYSTEM DROP FILESYSTEM CACHE '<name>' удаляет их вместе со всем остальным содержимым файлового кэша. Системная таблица system.query_cache и метрики QueryCacheEntries и QueryCacheBytes отражают только записи кэша запросов в памяти. Количество попаданий и промахов кэша запросов на диске отображается в виде событий «QueryCacheOnDiskHits» и «QueryCacheOnDiskMisses» в системной таблице system.events. Эти события относятся только к хранилищу на диске, тогда как «QueryCacheHits» и «QueryCacheMisses» по-прежнему считают обращения к кэшу запросов в целом, а «QueryCacheAgeSeconds» учитывает попадания в оба хранилища. Сначала проверяется кэш в памяти, поэтому количество попаданий в памяти равно разности «QueryCacheHits» и «QueryCacheOnDiskHits».