Зачем: контракт будущего 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>
11 KiB
Проталкивание условий сквозь представления над 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 годится как форма контракта. Две оговорки в контракт:
FINALвнутриods.event_vотключает автоматическийPREWHERE— на объёмах стенда это незаметно, но слабый фильтр по неключевой колонке при широкомSELECTчитает больше, чем безFINAL. Это свойствоFINAL, представления его не усугубляют.- Индекс срезает только то, что умеет анализировать: фильтр по колонке,
которую
dds.event_vвычислит нетривиальным выражением, до индекса может не доехать. Ключевые для срезов колонки — прежде всегоEventDate— стоит отдавать в DDS переименованием, а не пересчётом.