Learning Platform
Глоссарий Troubleshooting
Урок 07.03 · 20 мин
Средний
PREWHEREoptimize_move_to_prewhereI/O OptimizationMergeTreeColumn Pruning

PREWHERE: фильтрация до чтения

WHERE в колоночной СУБД уже эффективен — ClickHouse читает только столбцы из SELECT и WHERE. Но PREWHERE идёт дальше: он читает сначала только столбцы из условия фильтрации, применяет фильтр, и затем читает остальные столбцы только для строк, прошедших фильтр.

PREWHERE читает сначала столбец status (4 байта на строку). Если условие исключает 90% строк, ClickHouse читает остальные 10 столбцов только для 10% строк — колоссальная экономия I/O.


Механика PREWHERE

Обычный WHERE работает так:

  1. Прочитать все столбцы из SELECT + WHERE для гранулы
  2. Применить фильтр
  3. Вернуть подходящие строки

PREWHERE меняет порядок:

  1. Прочитать только столбцы из условия PREWHERE
  2. Применить фильтр — получить маску подходящих строк
  3. Прочитать остальные столбцы только для подходящих строк
WHERE vs. PREWHERE: путь чтения данных
WHERE: читаем все 11 столбцовWHERE: ClickHouse читает ВСЕ 11 столбцов (status + 10 других) для каждой гранулы. Полный I/O: 11 файлов .bin, все данные загружены в память.
Фильтр: status = error (10% строк)Фильтрация: применяем WHERE status = 'error' после чтения всех данных. 90% строк отбрасываются, но I/O уже выполнен для всех 11 столбцов.
I/O100% данных прочитаноВсе 11 .bin файлов прочитаны полностью для каждой гранулы. 90% прочитанных данных отброшено после фильтрации -- потраченный I/O.
PREWHERE: читаем 1 столбец (status)PREWHERE: сначала читаем ТОЛЬКО status.bin (1 столбец из 11). Это минимальный I/O -- один маленький файл.
Фильтр: 10% строк подходятФильтрация: применяем PREWHERE status = 'error'. Получаем маску: 10% строк подходят. Теперь мы знаем КАКИЕ строки нужны, не прочитав остальные столбцы.
Читаем 10 столбцов для 10% строкЧтение остальных 10 столбцов ТОЛЬКО для 10% строк, прошедших фильтр. 90% данных остальных столбцов никогда не читаются с диска.
I/O~19% данных прочитаноПрочитано: 1 полный столбец (status) + 10 столбцов * 10% строк = 1 + 1 = 2 столбца эквивалент. Вместо 11 столбцов -- экономия ~82% I/O.

Авто-промоция WHERE в PREWHERE

ClickHouse автоматически перемещает подходящие условия из WHERE в PREWHERE. Эта оптимизация включена по умолчанию:

-- Настройка авто-промоции (по умолчанию = 1)
SET optimize_move_to_prewhere = 1;

Условия перемещаются в PREWHERE если:

  • Столбец фильтра не является частью ORDER BY ключа (столбцы из ORDER BY и так фильтруются через primary.idx)
  • Столбец фильтра меньше по размеру чем остальные столбцы SELECT
  • Условие достаточно селективно (отбрасывает значительную часть строк)

Проверить, применился ли PREWHERE, можно через EXPLAIN SYNTAX:

EXPLAIN SYNTAX
SELECT * FROM events WHERE event_date = '2024-01-15' AND status = 'error'

Если auto-promotion сработал, вывод покажет:

SELECT * FROM events
PREWHERE status = 'error'
WHERE event_date = '2024-01-15'

Условие status = 'error' перемещено в PREWHERE, потому что status — маленький столбец и не входит в ORDER BY.


Ручной синтаксис PREWHERE

Можно указать PREWHERE явно, комбинируя с WHERE:

SELECT
    user_id, event_type, payload
FROM events
PREWHERE status = 'error'
WHERE event_date >= '2024-01-01'
  AND length(payload) > 1000

Порядок выполнения:

  1. Primary key pruning по event_date (если event_date в ORDER BY)
  2. PREWHERE: читается status.bin, фильтрация по status = ‘error’
  3. Чтение user_id.bin, event_type.bin, payload.bin для подходящих строк
  4. WHERE: фильтрация по event_date и length(payload)
TIP

Ручной PREWHERE полезен когда авто-промоция выбирает не тот столбец. Например, если ClickHouse продвигает дорогое выражение вместо дешёвого:

-- Принудительно ставим дешёвый фильтр в PREWHERE
SELECT * FROM events
PREWHERE is_deleted = 0           -- UInt8, 1 байт
WHERE complex_expression(payload)  -- дорогое вычисление

Взаимодействие с FINAL

Таблицы с Replacing/Collapsing семейством часто используют FINAL для дедупликации. PREWHERE и FINAL работают вместе, но требуют дополнительную настройку:

-- Для PREWHERE с FINAL обе настройки должны быть включены
SET optimize_move_to_prewhere = 1;
SET optimize_move_to_prewhere_if_final = 1;  -- по умолчанию = 0

Без optimize_move_to_prewhere_if_final авто-промоция PREWHERE отключена для запросов с FINAL. Это связано с тем, что FINAL меняет порядок обработки строк, и не все PREWHERE-условия безопасны в этом контексте.


Ограничения PREWHERE

ОграничениеПричина
Только MergeTree семействоPREWHERE требует колоночный формат хранения с отдельными .bin файлами
Нельзя использовать с ARRAY JOINМассивы расширяются до PREWHERE, нарушая маску строк
Условие PREWHERE не может использовать ALIAS столбцыALIAS вычисляется позже в pipeline
Не эффективен при низкой селективностиЕсли PREWHERE пропускает более 50% строк, выигрыш минимален — читается почти всё

Когда PREWHERE НЕ помогает

PREWHERE эффективен при высокой селективности фильтра. Если условие пропускает большинство строк — экономия I/O минимальна:

СелективностьПримерЭкономия I/O
1% проходит фильтрstatus = ‘critical’Огромная: остальные столбцы читаются для 1% строк
10% проходит фильтрstatus = ‘error’Значительная: 90% I/O сэкономлено
50% проходит фильтрis_active = 1Умеренная: половина I/O сэкономлена
95% проходит фильтрNOT is_deletedМинимальная: почти все строки читаются, а overhead PREWHERE добавлен
WARNING

PREWHERE на столбце, который пропускает 95% строк, может быть медленнее обычного WHERE: дополнительный проход по данным для построения маски не окупается, потому что почти все строки всё равно читаются.


Ключевые выводы

  1. PREWHERE читает столбцы фильтра первыми, затем остальные столбцы только для подходящих строк. При 90% селективности экономит более 80% I/O.
  2. optimize_move_to_prewhere (по умолчанию включен) автоматически продвигает подходящие WHERE-условия в PREWHERE.
  3. PREWHERE + FINAL требует дополнительно optimize_move_to_prewhere_if_final = 1.
  4. Только MergeTree — другие движки не поддерживают PREWHERE.
  5. Высокая селективность обязательна — PREWHERE на условии, пропускающем 95% строк, может ухудшить производительность.
Parquet Column Chunks: физика чтения столбцов Parse и Analyze: как запрос превращается в план

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Таблица events содержит 20 столбцов. Запрос SELECT * FROM events WHERE is_error = 1 использует PREWHERE на столбце is_error (UInt8). Как PREWHERE экономит I/O по сравнению с WHERE?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 8