PREWHERE: фильтрация до чтения
WHERE в колоночной СУБД уже эффективен — ClickHouse читает только столбцы из SELECT и WHERE. Но PREWHERE идёт дальше: он читает сначала только столбцы из условия фильтрации, применяет фильтр, и затем читает остальные столбцы только для строк, прошедших фильтр.
PREWHERE читает сначала столбец status (4 байта на строку). Если условие исключает 90% строк, ClickHouse читает остальные 10 столбцов только для 10% строк — колоссальная экономия I/O.
Механика PREWHERE
Обычный WHERE работает так:
- Прочитать все столбцы из SELECT + WHERE для гранулы
- Применить фильтр
- Вернуть подходящие строки
PREWHERE меняет порядок:
- Прочитать только столбцы из условия PREWHERE
- Применить фильтр — получить маску подходящих строк
- Прочитать остальные столбцы только для подходящих строк
Авто-промоция 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
Порядок выполнения:
- Primary key pruning по event_date (если event_date в ORDER BY)
- PREWHERE: читается status.bin, фильтрация по status = ‘error’
- Чтение user_id.bin, event_type.bin, payload.bin для подходящих строк
- WHERE: фильтрация по event_date и length(payload)
Ручной 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 добавлен |
PREWHERE на столбце, который пропускает 95% строк, может быть медленнее обычного WHERE: дополнительный проход по данным для построения маски не окупается, потому что почти все строки всё равно читаются.
Ключевые выводы
- PREWHERE читает столбцы фильтра первыми, затем остальные столбцы только для подходящих строк. При 90% селективности экономит более 80% I/O.
- optimize_move_to_prewhere (по умолчанию включен) автоматически продвигает подходящие WHERE-условия в PREWHERE.
- PREWHERE + FINAL требует дополнительно
optimize_move_to_prewhere_if_final = 1. - Только MergeTree — другие движки не поддерживают PREWHERE.
- Высокая селективность обязательна — PREWHERE на условии, пропускающем 95% строк, может ухудшить производительность.