Mark, Uncompressed и Compiled Expression Cache
ClickHouse использует три независимых уровня внутреннего кеширования, каждый из которых оптимизирует разные аспекты выполнения запросов. Понимание того, что именно кешируется на каждом уровне, позволяет правильно настроить сервер для конкретного типа нагрузки — от дашбордов до уникальных аналитических запросов.
Три уровня кеширования
Mark cache: кеш меток гранул
Каждая колонка в MergeTree-таблице хранит метки (marks) в файлах .mrk2. Метка указывает смещение в .bin файле для каждой гранулы данных. При выполнении запроса ClickHouse читает метки, чтобы найти нужные гранулы.
Mark cache хранит эти метки в памяти, устраняя чтение .mrk2 файлов с диска при каждом запросе.
-- Мониторинг mark cache
SELECT
metric,
formatReadableSize(value) AS readable_value
FROM system.asynchronous_metrics
WHERE metric LIKE 'MarkCache%';
-- Ожидаемые метрики:
-- MarkCacheBytes 2147483648 (2.00 GiB)
-- MarkCacheFiles 14523 (количество файлов в кеше)
Настройка размера mark cache (в config.xml или через ALTER SYSTEM):
-- В config.xml:
-- <mark_cache_size>5368709120</mark_cache_size> -- 5 GB (по умолчанию)
Mark cache особенно важен для cold start: первые запросы после перезапуска сервера читают .mrk2 файлы с диска и заполняют кеш. Последующие запросы к тем же таблицам работают быстрее.
Uncompressed cache: кеш разжатых блоков
При чтении данных ClickHouse читает сжатые блоки из .bin файлов и разжимает их. Uncompressed cache хранит уже разжатые блоки в памяти, избегая повторного чтения и разжатия.
-- Мониторинг uncompressed cache
SELECT
metric,
formatReadableSize(value) AS readable_value
FROM system.asynchronous_metrics
WHERE metric LIKE '%UncompressedCache%';
По умолчанию uncompressed_cache_size = 0 — кеш отключён.
Uncompressed cache эффективен только для повторных запросов к одним и тем же гранулам. Для уникальных полных сканов больших объёмов — overhead без пользы: кеш заполняется данными, которые не будут запрошены повторно, вытесняя полезные записи. Типичный сценарий где он помогает: дашборды с одинаковыми фильтрами, повторяющиеся отчёты по одним и тем же временным периодам.
Пример конфигурации для дашборд-нагрузки:
-- В config.xml (включить uncompressed cache, 4 GB):
-- <uncompressed_cache_size>4294967296</uncompressed_cache_size>
Compiled expression cache: JIT-кеш выражений
ClickHouse может JIT-компилировать выражения из WHERE, GROUP BY, HAVING в нативный машинный код с SIMD-оптимизациями. Compiled expression cache хранит уже скомпилированные выражения и переиспользует их при повторных запросах с одинаковой структурой.
Для включения JIT-компиляции:
-- Включить JIT-компиляцию для текущей сессии
SET compile_expressions = 1;
-- Запросы с повторяющимися выражениями выполнятся быстрее
SELECT
user_id,
sum(revenue * discount_factor + shipping_cost) AS total
FROM orders
GROUP BY user_id;
Мониторинг compiled expression cache:
-- Проверить размер compiled expression cache
SELECT
metric,
formatReadableSize(value) AS readable_value
FROM system.asynchronous_metrics
WHERE metric LIKE '%CompiledExpression%';
JIT-компиляция применяется не ко всем выражениям. ClickHouse компилирует только выражения, которые можно безопасно оптимизировать через LLVM. Сложные условия с подзапросами, внешними функциями или нетривиальными типами данных могут не компилироваться. Не рассчитывайте, что compile_expressions=1 ускорит все запросы.
JIT-расширение в ClickHouse 26.1
В исторических версиях JIT охватывал ограниченный набор путей: sum, count, min, max, avg, avgWeighted для числовых типов; expressions WHERE/GROUP BY с базовыми операциями; адаптер Nullable обрабатывался через wrapper-цикл, что добавляло indirection. В ClickHouse 26.1 JIT-инфраструктура была существенно расширена.
Что добавлено в 26.1:
- JIT для Nullable-агрегатов без wrapper-overhead. Раньше Nullable-колонки требовали внешнего цикла-обёртки, проверявшего null-битмаску перед вызовом native-кода. В 26.1 null-проверка инлайнится в скомпилированный код через LLVM, и сам цикл целиком становится JIT-блоком.
- JIT для Decimal128 / Decimal256. До 26.1 JIT покрывал только Decimal64 в ограниченных операциях. 26.1 добавил полные арифметические pipeline для Decimal128/256, включая
sum,avg,min,max. - Расширенный набор aggregate functions. Помимо классической шестёрки, JIT теперь покрывает
groupBitAnd,groupBitOr,groupBitXor,any,anyLast,argMin,argMax(для базовых числовых ключей). - JIT для
-Ifcombinator. Conditional агрегации видаsumIf,countIf,avgIfтеперь компилируются в один проход без отдельного branch-предсказания.
-- Включение всего стека JIT в 26.1
SET compile_expressions = 1;
SET compile_aggregate_expressions = 1;
SET compile_sort_description = 1;
SET min_count_to_compile_expression = 3;
SET min_count_to_compile_aggregate_expression = 3;
-- Запрос, который раньше не получал JIT (Nullable Decimal128 + If-combinator)
SELECT
user_id,
sumIf(revenue, status = 'paid') AS paid_revenue, -- Nullable Decimal128
avgIf(processing_ms, region = 'EU') AS eu_latency
FROM transactions
GROUP BY user_id;
Performance numbers в публичных бенчмарках 26.1 показывают ускорение до 1.5-2x на Nullable-aggregations и до 1.3x на Decimal128 sum/avg по сравнению с тем же запросом на 25.x. Эффект особенно заметен на запросах с большим GROUP BY на Nullable-колонках, где раньше доминировал overhead wrapper-цикла.
Чтобы проверить, какие части плана действительно скомпилированы JIT, используйте EXPLAIN actions = 1. В выводе вы увидите атрибут compiled у соответствующих ExpressionStep / AggregatingStep — это подтверждение, что LLVM-pipeline сгенерировал native-код, а не fallback-интерпретатор.
Мониторинг всех кешей
-- Все метрики кешей в одном запросе
SELECT
metric,
formatReadableSize(value) AS readable_value
FROM system.asynchronous_metrics
WHERE metric LIKE '%Cache%'
ORDER BY metric;
Сводная таблица трёх кешей
| Кеш | Настройка | По умолчанию | Что кеширует | Когда полезен |
|---|---|---|---|---|
| Mark cache | mark_cache_size | 5 GB | Метки гранул (.mrk2) | Всегда — ускоряет поиск гранул для каждого запроса |
| Uncompressed cache | uncompressed_cache_size | 0 (выключен) | Разжатые блоки (.bin) | Повторные запросы к одним гранулам, дашборды |
| Compiled expression cache | compiled_expression_cache_size | Авто | JIT-скомпилированные выражения | Повторяющиеся запросы с одинаковыми выражениями |
Ключевые выводы
- Mark cache (по умолчанию 5 GB) кеширует метки гранул и полезен для всех запросов — его рекомендуется оставлять включённым и достаточно большим.
- Uncompressed cache по умолчанию отключён. Включать только для рабочих нагрузок с повторяющимися запросами к одним и тем же гранулам (дашборды, отчёты). Для уникальных полных сканов — overhead без пользы.
- Compiled expression cache с
compile_expressions=1ускоряет повторяющиеся вычисления через JIT. В 26.1 JIT-покрытие расширено до Nullable-агрегатов без wrapper-overhead, Decimal128/256,-If-combinator и aggregate functionsargMin/argMax/groupBit*. Включайте такжеcompile_aggregate_expressions = 1и проверяйте черезEXPLAIN actions = 1атрибутcompiledна нужных steps. system.asynchronous_metrics WHERE metric LIKE '%Cache%'— основной инструмент мониторинга всех трёх кешей.formatReadableSize()переводит байтовые значения в читаемый формат (MiB, GiB) для удобного анализа.