Files
clickstream-data-platform/docs/research/2026-08-22-view-predicate-pushdown.md
ddadminandClaude Fable 5 86ee901dd0 docs(research): проверено проталкивание условий сквозь представления над FINAL
Зачем: контракт будущего dds.event_v зависит от того, доезжают ли условия
запроса сквозь цепочку обычных представлений до индексов таблицы с FINAL,
или представление разворачивается целиком и фильтруется потом (#128).

Что: research-заметка с опытами на живом стенде (EXPLAIN indexes/actions,
сверка read_rows по system.query_log) и сверкой с документацией ClickHouse
через MCP Context7. Ответ: условия доезжают сквозь два уровня, включая
переименования и анализируемые выражения; FINAL отключает только
автоматический PREWHERE. Цепочка представлений ODS -> DDS -> DM годится.

Проверка: только читающие запросы, состояние стенда не менялось; вложенное
представление эмулировано подзапросом.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 03:33:55 -04:00

11 KiB
Raw Permalink Blame History

Проталкивание условий сквозь представления над FINAL

Дата исследования: 2026-08-22. Тикет #128, карта #127.

Вопрос: пробрасывает ли ClickHouse условия запроса внутрь обычного представления над таблицей с FINAL (ods.event_v) и внутрь вложенного представления над ним (будущее dds.event_v) — или разворачивает представление целиком и фильтрует потом.

Короткий вывод

  • Пробрасывает. Обычное представление — это подзапрос в FROM, и условие внешнего запроса доезжает сквозь него до партиций и первичного ключа таблицы. Проверено планом и счётчиком прочитанных строк.
  • Второй уровень вложенности ничего не ломает: сквозь подзапрос над ods.event_v с переименованными колонками условие доезжает так же — срезание партиций совпадает с прямым запросом к ods.event_dist FINAL один в один.
  • Условие доезжает даже сквозь вычисленную колонку: фильтр по toStartOfWeek(EventDate) анализируется индексом MinMax.
  • Единственное, что FINAL отнимает, — автоматический перенос условия в PREWHERE: настройка optimize_move_to_prewhere_if_final по умолчанию выключена. Срезание по индексам при этом работает полностью; теряется только оптимизация порядка чтения колонок внутри уже выбранных гранул.
  • Для контракта dds.event_v это значит: цепочка обычных представлений ODS → DDS годится, представление до фильтра не материализуется.

Как проверяли

Живой стенд, ClickHouse 26.3.17.56, enable_analyzer = 1 (умолчание). Только читающие запросы с ноды 1: EXPLAIN indexes = 1, EXPLAIN actions = 1, SELECT со сверкой по system.query_log. Вложенное представление не создавали — состояние стенда не менялось; второй уровень эмулирован эквивалентным подзапросом.

Устройство пары: ods.event_rep — ReplicatedReplacingMergeTree(_load_ts), PARTITION BY EventDate, ORDER BY (CounterID, EventDate, intHash32(ClientID), WatchID); на момент опыта 16 активных кусков в 8 партициях, 24 гранулы. ods.event_v — SELECT * FROM ods.event_dist FINAL.

Опыт 1. Фильтр по ключу партиции сквозь представление

EXPLAIN indexes = 1
SELECT count() FROM ods.event_v WHERE EventDate = '2026-06-01';

План читает ods.event_rep с условием EventDate in [20605, 20605] во всех трёх индексах — MinMax, Partition, PrimaryKey: Parts: 2/16, Granules: 3/24. Прямой запрос SELECT count() FROM ods.event_dist FINAL WHERE EventDate = '2026-06-01' даёт те же цифры. Представление ничего не прячет от анализа индексов.

Опыт 2. Второй уровень: подзапрос над представлением

Будущее dds.event_v — представление над ods.event_v с переименованием в язык бизнес-модели. Эмуляция:

EXPLAIN indexes = 1
SELECT count()
FROM (
    SELECT WatchID AS watch_id, EventDate AS event_date,
           lower(EventType) AS event_type
    FROM ods.event_v
) WHERE event_date = '2026-06-01';

Условие по переименованной колонке доехало до таблицы: Parts: 2/16, Granules: 3/24 — цифры совпали с опытом 1. Два уровня разворачивания и переименование колонок проталкиванию не мешают.

Отдельно проверена вычисленная колонка:

SELECT count()
FROM (SELECT toStartOfWeek(EventDate) AS event_week FROM ods.event_v)
WHERE event_week = '2026-06-01';

В плане условие toStartOfWeek(EventDate) in [20605, 20605] стоит в индексе MinMax и срезает Parts: 0/16 — ноль корректен: при умолчании функции ни одна неделя не начинается в этот понедельник. То есть даже фильтр по выражению от ключевой колонки анализируется индексом, пока функция ему понятна.

Опыт 3. Фильтр по ключу сортировки

EXPLAIN indexes = 1
SELECT count() FROM ods.event_v WHERE CounterID = 42150607;

Условие CounterID in [...] дошло до PrimaryKey (Search Algorithm: binary search). Гранулы не срезались (24/24) — но это свойство данных, а не представления: счётчик один, он есть в каждой грануле.

Опыт 4. PREWHERE и FINAL

По колонке вне ключа (EventType = 'pageview', EXPLAIN actions = 1, запрос с широким списком колонок):

  • ods.event_rep без FINAL — условие уехало в Prewhere filter;
  • ods.event_rep FINAL и ods.event_v — условие осталось обычным Filter над шагом чтения.

Причина — настройка optimize_move_to_prewhere_if_final, на стенде 0 (умолчание). Это цена самого FINAL, а не представления: прямой запрос с FINAL ведёт себя так же.

Опыт 5. Счётчик прочитанных строк

План — обещание; исполнение сверено по system.query_log (read_rows начального запроса, сумма по кластеру):

Запрос read_rows
ods.event_v без фильтра 401 185
ods.event_v WHERE EventDate = '2026-06-01' 50 626
подзапрос над ods.event_v, фильтр по event_date 50 626

Фильтр сквозь представление читает только партиции своего дня, вложенность не добавляет ни строки. 401 185 — все физические строки обеих нод.

Что говорит документация

Сверено через MCP Context7 22 августа 2026 года, библиотека /websites/clickhouse.

  • Обычное представление не хранит данных: при обращении его запрос исполняется как подзапрос в FROM — SELECT a, b, c FROM view эквивалентен SELECT a, b, c FROM (SELECT ...) (sql-reference/statements/create/view).
  • Проталкивание предикатов в подзапросы — штатная работа оптимизатора; анализатор доводит фильтры до хранилища, включая срезание партиций и первичного ключа (changelog 2026, настройки семейства allow_push_predicate_*).
  • optimize_move_to_prewhere_if_final управляет автоматическим PREWHERE для запросов с FINAL (settings/session-settings); на стенде она в умолчании 0, что и видно в опыте 4.
  • do_not_merge_across_partitions_select_final = 1 заставляет FINAL обрабатывать партиции независимо; с 2026 года значение 1 включает режим безусловно, а автоматику решает отдельная настройка enable_automatic_decision_for_merging_across_partitions_for_final (changelog 2026). К ods.event режим применим: партиция — EventDate, версии одного события всегда лежат в одном дне. На стенде настройка 0, и трогать её не понадобилось — вопрос тикета решился без неё.
  • FINAL поверх подзапроса не работает — модификатор обязан стоять у самой таблицы (guides/.../analyzer, «Fixing Invalid FINAL Usage»). Наша цепочка это соблюдает: FINAL живёт в один слой, внутри ods.event_v.

Вывод для контракта dds.event_v

Сомнение владельца — «представление материализуется до фильтра» — не подтвердилось. Условия по ключу партиции и ключу сортировки доезжают до индексов сквозь два уровня обычных представлений, переименование колонок и анализируемые выражения; исполняется это, а не только планируется.

Цепочка обычных представлений ODS → DDS → DM годится как форма контракта. Две оговорки в контракт:

  1. FINAL внутри ods.event_v отключает автоматический PREWHERE — на объёмах стенда это незаметно, но слабый фильтр по неключевой колонке при широком SELECT читает больше, чем без FINAL. Это свойство FINAL, представления его не усугубляют.
  2. Индекс срезает только то, что умеет анализировать: фильтр по колонке, которую dds.event_v вычислит нетривиальным выражением, до индекса может не доехать. Ключевые для срезов колонки — прежде всего EventDate — стоит отдавать в DDS переименованием, а не пересчётом.