Query Cache
Query Cache — это механизм конвейерного движка выполнения Apache Doris, который кэширует промежуточные результаты агрегации на уровне tablet и ускоряет повторяющиеся агрегирующие запросы.
Что проверить перед началом
Перед использованием Query Cache убедитесь, что:
- Запрос обращается к внутренней таблице OLAP (а не к внешней таблице, такой как Hive/JDBC/Iceberg/Hudi/Paimon)
- Запрос является агрегирующим (содержит
GROUP BYили агрегатные функции) - План запроса соответствует шаблону
AggregationNode → OlapScanNode - Запрос не содержит узлов
JOIN,SORT,UNIONилиWINDOW - Запрос не зависит от недетерминированных функций, таких как
now(),rand()илиuuid() - Задано
enable_query_cache = true
Определение одним предложением
Query Cache кэширует результаты агрегации на уровне tablet в конвейерном движке выполнения. Если контекст выполнения последующего запроса совпадает, механизм сразу возвращает кэшированные данные, позволяя избежать повторного сканирования и вычислений.
Зачем нужен Query Cache
В аналитических сценариях один и тот же агрегирующий запрос часто выполняется многократно, хотя исходные данные не меняются. Например:
SELECT region, SUM(revenue) FROM orders WHERE dt = '2024-01-01' GROUP BY region;
SELECT region, SUM(revenue) FROM orders WHERE dt = '2024-01-01' GROUP BY region;
При каждом выполнении повторно сканируются те же tablet и заново вычисляется результат, что приводит к лишним затратам CPU и ввода-вывода. Query Cache кэширует промежуточные результаты агрегации и сразу возвращает их при попадании в кэш, значительно сокращая задержку.
- Эта функция применяется только к агрегирующим запросам к внутренним таблицам OLAP. Обычное сканирование, JOIN, сортировки и подобные операции не используют Query Cache.
- Внешние таблицы не поддерживаются (Hive, JDBC, Iceberg, Hudi, Paimon и другие).
Как это работает
Поддерживаемые шаблоны запросов
Кэш могут использовать только фрагменты, дерево плана выполнения которых соответствует одному из следующих шаблонов:
AggregationNode → OlapScanNode: одностадийная агрегация непосредственно над сканированием.AggregationNode → AggregationNode → OlapScanNode: двухстадийная агрегация над сканированием.
Между узлом агрегации и узлом сканирования допускаются промежуточные узлы, такие как FilterNode и ProjectNode. Однако кэшируемое поддерево не должно содержать узлы JoinNode, SortNode, UnionNode, WindowNode или ExchangeNode.
Три компонента ключа кэша
| Компонент | Описание |
|---|---|
| Дайджест SQL | Хеш SHA-256, вычисляемый по нормализованному дереву плана выполнения (агрегатные функции, выражения группировки, предикаты фильтрации, не связанные с партициями, проецируемые столбцы и переменные сеанса, влияющие на результат). Семантически эквивалентные запросы дают одинаковый дайджест. |
| Список идентификаторов tablet | Отсортированный список идентификаторов tablet, назначенных текущему экземпляру конвейера. |
| Диапазон tablet | Допустимый диапазон сканирования для каждого tablet, определяемый по предикатам партиционирования (см. Особенности работы с партициями и фильтрами). |
Условия инвалидации кэша
| Условие срабатывания | Описание |
|---|---|
| Изменение данных | INSERT, DELETE, UPDATE или компактизация увеличивают номер версии tablet; последующие запросы сравнивают версии, и их несовпадение приводит к промаху кэша. |
| Изменение схемы | ALTER TABLE изменяет структуру таблицы, что приводит к изменению плана выполнения и дайджеста. |
| Вытеснение LRU | Когда объём памяти кэша превышает лимит, записи вытесняются по алгоритму LRU-K (K=2); для добавления новой записи к ней необходимо обратиться как минимум дважды. |
| Удаление устаревших записей | Записи старше 24 часов автоматически удаляются периодической задачей очистки. |
| Принудительное обновление | Если задано query_cache_force_refresh = true, кэш игнорируется и запрос выполняется заново. |
Порядок выполнения
Первое выполнение (промах кэша):
- Оператор сканирования читает данные из tablet в обычном режиме.
- Оператор агрегации вычисляет результат.
- Результат передаётся последующим потребителям и накапливается для записи в кэш.
- После завершения выполнения результат записывается в кэш, если накопленные данные не превышают лимиты размера или количества строк для одной записи.
Последующее выполнение (попадание в кэш):
- Оператор сканирования обнаруживает попадание в кэш и пропускает диапазон сканирования. Данные из tablet не читаются.
- Оператор агрегации не получает входных данных и ничего не выдаёт.
- Оператор источника кэша напрямую предоставляет кэшированные блоки данных.
- Если порядок столбцов отличается от порядка в записи кэша (например,
SELECT a, bиSELECT b, aдают одинаковый дайджест), столбцы автоматически переставляются.
Особенности работы с партициями и фильтрами
Чтобы добиться высокой доли попаданий в кэш, необходимо понимать, как предикаты партиционирования и выражения фильтрации взаимодействуют с Query Cache.
Предикаты партиционирования RANGE по одному столбцу
Для таблиц с партиционированием RANGE по одному столбцу предикаты партиционирования обрабатываются особым образом:
- Предикаты партиционирования исключаются из дайджеста. Система вычисляет пересечение диапазона предиката с фактическим диапазоном каждой партиции и добавляет его к ключу кэша в виде строки диапазона tablet.
- Два запроса, которые отличаются только диапазоном фильтрации по партициям, могут совместно использовать кэш для общих tablet.
Пример: таблица orders разбита на ежедневные партиции по столбцу dt.
-- Query A
SELECT region, SUM(revenue) FROM orders
WHERE dt >= '2024-01-01' AND dt < '2024-01-03' GROUP BY region;
-- Query B
SELECT region, SUM(revenue) FROM orders
WHERE dt >= '2024-01-02' AND dt < '2024-01-04' GROUP BY region;
- Запрос A сканирует партиции
2024-01-01и2024-01-02. - Запрос B сканирует партиции
2024-01-02и2024-01-03. - Дайджест и диапазон tablet для партиции
2024-01-02совпадают, поэтому запрос B повторно использует кэш запроса A для2024-01-02, и пересчитать требуется только партицию2024-01-03.
Таблицы с партиционированием RANGE по нескольким столбцам / LIST / без партиционирования
Для таблиц с партиционированием RANGE по нескольким столбцам, партиционированием LIST или без партиционирования предикаты партиционирования нельзя исключить: они включаются непосредственно в дайджест. Даже небольшие различия в этих предикатах приводят к разным дайджестам и промахам кэша.
Выражения фильтрации, не связанные с партициями
Выражения фильтрации, не связанные с партициями (например, WHERE status = 'active'), включаются в дайджест нормализованного плана выполнения. Два запроса могут совместно использовать кэш только в том случае, если их нормализованные выражения фильтрации семантически идентичны.
| Запрос 1 | Запрос 2 | Совместное использование кэша |
|---|---|---|
WHERE status = 'active' | WHERE status = 'active' | Да (одинаковый дайджест) |
WHERE status = 'active' | WHERE status = 'inactive' | Нет (разные дайджесты) |
WHERE status = 'active' AND region = 'ASIA' | WHERE region = 'ASIA' AND status = 'active' | Да (после нормализации порядок не имеет значения) |
Переменные сеанса
Переменные сеанса, влияющие на результаты запросов (такие как time_zone, sql_mode и sql_select_limit), включаются в дайджест. Изменение любой из этих переменных между двумя запросами приводит к другому ключу кэша и промаху.
Условия, при которых Query Cache отключается
| Условие | Причина |
|---|---|
| К фрагменту применяется фильтр времени выполнения | Значения фильтра времени выполнения неизвестны на этапе планирования, поэтому кэширование привело бы к неверным результатам. |
Содержит недетерминированные выражения (rand(), now(), uuid(), UDFs и другие) | Даже при одинаковых входных данных результаты различаются от выполнения к выполнению. |
| Кэшируемое поддерево содержит узел JOIN, SORT, UNION или WINDOW | Поддерживается только шаблон «агрегация со сканированием». |
Узел сканирования не является OlapScanNode (например, сканируется внешняя таблица) | Кэш опирается на идентификаторы и версии tablet, которых нет у внешних таблиц. |
Почему Query Cache не поддерживает внешние таблицы
Query Cache опирается на три свойства, присущие только внутренним таблицам OLAP:
- Организация данных на основе tablet: ключ кэша содержит идентификаторы tablet и диапазон сканирования для каждого из них. Внешние таблицы хранятся во внешних системах, таких как HDFS, S3 или JDBC, и не используют понятие tablet.
- Инвалидация на основе версий: каждый внутренний tablet имеет монотонно возрастающий номер версии, по которому кэш определяет устаревшие данные. Внешние таблицы не предоставляют Doris такой механизм версий.
- Требование OlapScanNode: логика нормализации плана выполнения распознаёт только
OlapScanNodeкак допустимый узел сканирования под точкой кэширования агрегации.
Для кэширования запросов к внешним таблицам используйте SQL Cache.
Параметры конфигурации
Переменные сеанса (FE)
| Параметр | Описание | По умолчанию |
|---|---|---|
enable_query_cache | Главный переключатель, включающий или отключающий Query Cache. | false |
query_cache_force_refresh | Если задано true, кэшированный результат игнорируется и запрос выполняется заново; новый результат по-прежнему записывается в кэш. | false |
query_cache_entry_max_bytes | Максимальный размер одной записи кэша в байтах; результаты фрагментов, превышающие этот лимит, не кэшируются. | 5242880 (5 MB) |
query_cache_entry_max_rows | Максимальное количество строк в одной записи кэша; результаты фрагментов, превышающие этот лимит, не кэшируются. | 500000 |
Настройки BE (be.conf)
| Параметр | Описание | По умолчанию |
|---|---|---|
query_cache_size | Общий объём памяти Query Cache на каждом BE (MB). | 512 |
Настройки query_cache_max_size_mb и query_cache_elasticity_size_mb в be.conf управляют устаревшим SQL Result Cache. Они не относятся к Query Cache уровня конвейера, описанному в этой статье. Не путайте эти механизмы.
Примеры использования
Шаг 1: включить Query Cache
Цель: включить главный переключатель Query Cache.
SET enable_query_cache = true;
Описание: эта переменная действует в пределах сеанса, поэтому её нужно включать в каждом соединении. Также можно задать её значение по умолчанию как глобальную переменную FE.
Шаг 2: выполнить типичный агрегирующий запрос
Цель: вызвать запись в кэш и чтение из него.
-- First execution: cache miss, compute the result and write it to the cache
SELECT region, SUM(revenue), COUNT(*)
FROM orders
WHERE dt = '2024-01-15' AND status = 'completed'
GROUP BY region;
-- Second execution: cache hit, return the result directly from the cache
SELECT region, SUM(revenue), COUNT(*)
FROM orders
WHERE dt = '2024-01-15' AND status = 'completed'
GROUP BY region;
Описание: дайджест SQL, список идентификаторов tablet и диапазон tablet при втором выполнении совпадают с первым, поэтому происходит попадание в кэш.
Шаг 3: проверить попадание в кэш по профилю
Цель: убедиться, что запрос действительно использует кэш.
После выполнения запроса откройте профиль и найдите раздел CacheSourceOperator:
| Поле профиля | Значение |
|---|---|
HitCache: true | Запрос получил результат из кэша. |
HitCache: false, InsertCache: true | Произошёл промах, но результат успешно записан в кэш. |
HitCache: false, InsertCache: false | Произошёл промах, а результат оказался слишком большим для кэширования. |
CacheTabletId | Идентификатор tablet, к которому относится запись кэша. |
Шаг 4: принудительно обновить кэш
Цель: проигнорировать существующий кэш и заново вычислить результат (например, если вы подозреваете, что кэшированные данные неверны).
-- Force the next query to skip the cache and recompute the result
SET query_cache_force_refresh = true;
SELECT region, SUM(revenue) FROM orders WHERE dt = '2024-01-15' GROUP BY region;
-- Reset
SET query_cache_force_refresh = false;
Описание: после принудительного обновления новый результат по-прежнему записывается в кэш.
Сравнение сценариев использования
| Сценарий | Применимость | Причина |
|---|---|---|
| Панели мониторинга или инструменты BI, многократно выполняющие один и тот же агрегирующий SQL | Да | Дайджест и tablet полностью совпадают, обеспечивая высокую долю попаданий в кэш. |
| Отчёты T+1 (данные загружаются раз в день) | Да | Последующие запросы в тот же день могут попадать в кэш. |
| Агрегирующие запросы с пересекающимися диапазонами дат | Да | Партиционирование RANGE по одному столбцу позволяет совместно использовать записи кэша на уровне tablet. |
| Обычное сканирование SELECT, JOIN, сортировки и оконные функции | Нет | Поддерживается только шаблон «агрегация со сканированием». |
| Внешние таблицы (Hive, JDBC, Iceberg, Hudi, Paimon) | Нет | Нет механизма tablet или версий. Используйте SQL Cache. |
| Часто обновляемые таблицы | Нет | Версии tablet быстро меняются, что приводит к низкой доле попаданий в кэш. |
Запросы, содержащие now(), rand(), uuid() или UDFs | Нет | Недетерминированные результаты отключают кэш. |
| Запросы, зависящие от фильтров времени выполнения | Нет | Значения фильтров времени выполнения неизвестны на этапе планирования. |
Примечания
- Кэш не сохраняется: Query Cache находится в памяти BE и очищается при перезапуске BE.
- Потребление памяти: кэшированные блоки данных занимают память BE. Следите за использованием памяти и при необходимости изменяйте
query_cache_size. - Допуск в кэш по LRU-K: когда кэш заполнен, для добавления новой записи к ней необходимо обратиться как минимум дважды (K=2). Это предотвращает заполнение кэша результатами редко выполняемых запросов.
Устранение неполадок
| Симптом | Возможная причина | Решение |
|---|---|---|
Постоянно появляется HitCache: false | Не включён enable_query_cache. | Выполните SET enable_query_cache = true. |
HitCache: false, InsertCache: false | Результат для одной записи слишком велик и превышает query_cache_entry_max_bytes или _max_rows. | Увеличьте соответствующий порог или добавьте фильтры, чтобы уменьшить размер результата. |
В плане отсутствует CacheSourceOperator | План содержит JOIN/SORT/UNION/WINDOW или к нему применяется фильтр времени выполнения. | Перепишите SQL в соответствии с шаблоном «агрегация со сканированием». |
| Таблица является внешней | Query Cache не поддерживает внешние таблицы. | Используйте SQL Cache. |
| Данные не изменились, но кэш по-прежнему даёт промахи | Изменилась схема или переменная сеанса либо задано query_cache_force_refresh = true. | Проверьте историю ALTER, сравните переменные сеанса и сбросьте query_cache_force_refresh. |
| Доля попаданий в кэш очень низкая | Tablet часто обновляется или подвергается компактизации. | Уменьшите частоту записи или включайте кэш только для редко обновляемых таблиц. |
| Растёт нагрузка на память BE | Задано слишком большое значение query_cache_size. | Уменьшите query_cache_size и перезапустите BE. |
Частые вопросы
Вопрос 1: чем Query Cache отличается от SQL Cache?
| Критерий | Query Cache | SQL Cache |
|---|---|---|
| Гранулярность кэша | Промежуточные результаты агрегации на уровне tablet. | Итоговый результат всего SQL-оператора. |
| Поддерживаемые запросы | Только агрегирующие запросы к внутренним таблицам OLAP. | Любые запросы, включая запросы к внешним таблицам. |
| Возможность совместного использования | Разные SQL-операторы могут совместно использовать записи кэша на уровне tablet. | Попадание возможно только при точном совпадении текста SQL. |
| Инвалидация | Сразу после изменения версии tablet. | На основе версий партиций или времени. |
Вопрос 2: начнутся ли попадания в кэш сразу после его включения?
Нет. При первом выполнении происходит «промах кэша с записью в кэш»; попадания возможны только при втором и последующих выполнениях. Кроме того, LRU-K (K=2) требует как минимум двух обращений к новой записи, прежде чем она будет фактически добавлена в кэш.
Вопрос 3: можно ли кэшировать агрегации с JOIN?
Нет. Наличие JoinNode в кэшируемом поддереве отключает Query Cache для этого фрагмента. Попробуйте переписать запрос так, чтобы сначала выполнялась агрегация, а затем JOIN, или используйте материализованное представление.
Вопрос 4: нужен ли прогрев кэша после перезапуска BE?
Да. Query Cache хранится в памяти и очищается при перезапуске. Для прогрева можно выполнять основные агрегирующие SQL-запросы в часы низкой нагрузки.
Вопрос 5: как убедиться, что действительно произошло попадание в кэш?
После выполнения SQL проверьте поле HitCache оператора CacheSourceOperator в профиле.
Итоги
Query Cache — это механизм оптимизации на уровне конвейера в Doris, который кэширует промежуточные результаты агрегации на уровне tablet. Его основные особенности:
- Применяется только к агрегирующим запросам к внутренним таблицам OLAP.
- Автоматически инвалидирует кэш на основе версий tablet.
- Выделяет предикаты партиционирования из дайджеста, позволяя запросам с пересекающимися диапазонами партиций совместно использовать кэш.
- Ограничивает размер и количество строк для каждой записи, чтобы слишком большие результаты не занимали память кэша.
- Использует политику вытеснения LRU-K (K=2), чтобы поддерживать высокую эффективность кэша.