Зачем нужна оптимизация PREWHERE в ClickHouse

ClickHouse примеры курсы обучение, Архитектура данных курсы примеры обучение, дата-инженерия и архитектура данных примеры курсы обучение, Школа Больших Данных Учебный Центр Коммерсант

Как ускорить выполнение SQL-запроса в ClickHouse, сократив объем сканируемых данных с помощью оператора PREWHERE: практический пример простой, но эффективной оптимизации.

Как работает оператор PREWHERE в ClickHouse

ClickHouse имеет ряд многоуровневых оптимизаций, благодаря которым позволяет анализировать огромные объемы данных почти в реальном времени. Одной из таких оптимизаций является PREWHERE, которая сокращает объем сканируемых данных и повышает скорость выполнения запроса. Эта оптимизация входит в ClickHouse с самого первого выпуска и считается уникальной, по крайней мере, по названию. Именно само ключевое слово PREWHERE встречается только в ClickHouse, хотя концептуально похожие оптимизации есть и в других СУБД, например, Vertica, Oracle,  Apache Impala, Greenplum и PostgreSQL поддерживают Predicate Pushdown — механизм, который позволяет применять фильтры до чтения всех данных. В MySQL и MariaDB есть Condition Pushdown для работы с подзапросами и внешними таблицами, в Apache Druid и Snowflake существуют похожие внутренние оптимизации для раннего применения фильтров.

Однако, использование PREWHERE в ClickHouse требует явного указания этого в синтаксисе и предоставляет дата-инженеру прямой контроль над этой оптимизацией. Можно комбинировать PREWHERE и WHERE в одном запросе, чтобы повысить его гибкость. Также можно сочетать PREWHERE с другими оптимизациями, такими как разреженные индексы, усиливая их эффект.

В обычном SQL-запросе с условием WHERE сначала читаются все столбцы таблицы, а затем применяется фильтрация. Особенность PREWHERE в том, что ClickHouse сначала читает только те столбцы, которые упомянуты в этом условии, отфильтровывает подходящие данные и только после этого читает значительно меньше строк из остальных столбцов.

Это особенно эффективно для широких таблиц со множеством столбцов, когда условие фильтрации отсекает большую часть данных или использует столбцы с низкой кардинальностью, т.е. данные в которых повторяются часто. Например, создадим таблицу логов веб-сервера.

CREATE TABLE IF NOT EXISTS http_logs (
    client_ip String,
    request_method String,
    request_path String,
    timestamp DateTime,
    response_status UInt16
) ENGINE = MergeTree()
ORDER BY (timestamp, client_ip);

Наполним эту таблицу случайными данными:

INSERT INTO http_logs (client_ip, request_method, request_path, timestamp, response_status)
SELECT
    IPv4NumToString(rand() % 4294967295) AS client_ip,
    arrayElement(['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], rand() % 5 + 1) AS request_method,
    arrayElement(['/home', '/login', '/users', '/products', '/cart', '/checkout'], rand() % 6 + 1) AS request_path,
    toDateTime('2025-04-01 00:00:00') + INTERVAL rand() % (30*86400) SECOND AS timestamp,
    arrayElement([200, 201, 302, 404, 501, 503], rand() % 6 + 1) AS response_status
FROM numbers(1000);

Получим из этой таблицы первые 10 случаев HTTP-ответов со статусом 404, случившихся за период с 30 апреля по 1 мая 2025 года, с указанием IP-адреса клиента, метода запроса, запрашиваемого пути и момента времени появления ответа. Синтаксис SQL-запроса с условием PREWHERE для этого случая выглядит так:

SELECT 
    client_ip,
    request_method,
    request_path,
    response_status,
    timestamp
FROM http_logs
PREWHERE response_status = 404
WHERE timestamp BETWEEN '2025-04-01 00:00:00' AND '2025-05-01 00:00:00'
ORDER BY timestamp
LIMIT 10;

В этом запросе ClickHouse выполняет следующие шаги:

  • сперва читает только поле response_status и отфильтрует только строки со статусом 404, т.к. это указано в условии PREWHERE. Нужный статус ответа указан в условии PREWHERE, т.к. статус HTTP-ответа имеет низкую кардинальность и занимает мало места, тогда как URL-пути и другие поля могут быть значительно больше по размеру. Кроме того, ошибки 404 обычно составляют лишь небольшой процент от всех HTTP-ответов, поэтому фильтрация по этому полю существенно сокращает объем данных для последующей обработки;
  • затем применяет фильтр по timestamp из условия WHERE;
  • после этого читает остальные запрошенные поля (client_ip, request_method, request_path) для отфильтрованных строк.

Проверим результаты выполнения запросов в песочнице fiddle:

Выполнение запросов в песочнице ClickHouse
Выполнение запросов в песочнице ClickHouse

Если не использовать PREWHERE, т.е. оставить только оператор WHERE в SQL-запросе, ClickHouse прочитает все запрошенные столбцы, а затем применит фильтр. В этом случае объем сканируемых данных будет больше, а значит, запрос будет выполняться медленнее.  При работе с вложенными JSON-структурами оптимизация PREWHERE позволяет читать только нужные части JSON, что значительно повышает производительность. Оператор PREWHERE наиболее эффективен для широких таблиц и больших сканирований с селективными фильтрами, т.к. ClickHouse фильтрует данные столбец за столбцом, загружая только то, что действительно необходимо. Это радикально снижает затраты на дисковый ввод-вывод, особенно для широких таблиц. Поэтому PREWHERE рекомендуется использовать в следующих случаях:

  • запрос содержит фильтрацию по столбцам с небольшим объемом данных;
  • запрос выбирает множество столбцов;
  • условие фильтрации отсекает большое количество строк.

Во многих случаях ClickHouse автоматически преобразует подходящие условия из WHERE в PREWHERE, даже если этот оператор явно не указан в запросе, но настройка optimize_move_to_prewhere включена, т.е. равно true. По умолчанию это так и есть, но ее можно отключить вручную. Оптимизатор анализирует условия и определяет, какие из них выгоднее использовать в качестве PREWHERE., чтобы сократить объем чтения данных. Меньшие столбцы сканируются быстрее, а к моменту обработки больших столбцов большинство гранул уже отфильтрованы. Поскольку все столбцы имеют одинаковое количество строк, размер столбца в первую очередь определяется его типом данных, например, столбец, UInt16 обычно намного меньше столбца со строковыми данными. ClickHouse следует этой стратегии по умолчанию, начиная с версии 23.2 , сортируя столбцы фильтра PREWHERE для многошаговой обработки в порядке возрастания несжатого размера. Начиная с версии 23.11 , дополнительная статистика столбцов может дополнительно улучшить ситуацию, выбирая порядок обработки фильтров на основе фактической селективности данных, а не только размера столбца.

Чтобы убедиться, что PREWHERE помогает запросам, можно сравнить производительность их выполнения с включенной функцией optimize_move_to_prewhere setting и без нее, добавив соответствующую настройку в SQL-запрос. Например, следующий запрос выключает PREWHERE:

SELECT 
    client_ip,
    request_method,
    request_path,
    response_status,
    timestamp
FROM http_logs
WHERE response_status = 404 AND timestamp BETWEEN '2025-04-01 00:00:00' AND '2025-05-01 00:00:00'
ORDER BY timestamp
LIMIT 10
SETTINGS optimize_move_to_prewhere = false;

Важно, что PREWHERE сокращает количество считываемых данных, а не обрабатываемых строк: ClickHouse обрабатывает одинаковое количество строк в обеих версиях запроса: с оператором PREWHERE и без него. Однако при использовании PREWHERE не все значения столбцов нужно загружать для каждой обработанной строки.

В заключение отметим, что PREWHERE работает только с табличными движками семейства MergeTree. В запросе может быть только одно условие PREWHERE. Кроме того, иногда применение PREWHERE к столбцу с большим объемом данных может снизить производительность запроса.

Разобравшись с тем, как работает эта оптимизация, в следующей статье рассмотрим ее техническую реализацию, проанализировав планы выполнения запросов.

Освойте ClickHouse на специализированных курсах в нашем лицензированном учебном центре обучения и повышения квалификации для разработчиков, менеджеров, архитекторов, инженеров, администраторов, Data Scientist’ов и аналитиков Big Data в Москве:

Источники

  1. https://clickhouse.com/docs/optimize/prewhere
  2. https://clickhouse.com/blog/clickhouse-gets-lazier-and-faster-introducing-lazy-materialization
  3. https://fiddle.clickhouse.com/
Я даю свое согласие на обработку персональных данных и соглашаюсь с политикой конфиденциальности.