# Проталкивание условий сквозь представления над 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. Фильтр по ключу партиции сквозь представление ```sql 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` с переименованием в язык бизнес-модели. Эмуляция: ```sql 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. Два уровня разворачивания и переименование колонок проталкиванию не мешают. Отдельно проверена вычисленная колонка: ```sql 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. Фильтр по ключу сортировки ```sql 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 переименованием, а не пересчётом.