Перейти к основному содержимому
Перейти к основному содержимому

Materialized Views

ClickHouse Supported

Материализация materialized_view должна быть SELECT‑запросом к существующей (исходной) таблице. В отличие от PostgreSQL, materialized view в ClickHouse не является «статичной» (и не имеет соответствующей операции REFRESH). Вместо этого она работает как триггер вставки, добавляя новые строки в целевую таблицу, применяя заданное SELECT‑преобразование к строкам, вставляемым в исходную таблицу. См. документацию по materialized view в ClickHouse для получения дополнительных сведений о том, как materialized view работает в ClickHouse.

Примечание

Общие концепции материализаций и их общие настройки (engine, order_by, partition_by и т. д.) см. на странице Materializations.

Как управляется целевая таблица

Когда вы используете материализацию materialized_view, dbt-clickhouse создает как materialized view, так и целевую таблицу, в которую вставляются преобразованные строки. Существуют два способа управления целевой таблицей:

ПодходОписаниеСтатус
Неявная целевая таблицаdbt-clickhouse автоматически создает и управляет целевой таблицей в рамках той же модели. Схема целевой таблицы выводится из SQL MV.Stable
Явная целевая таблицаВы определяете целевую таблицу как отдельную материализацию table и ссылаетесь на нее из своей MV-модели с помощью макроса materialization_target_table(). MV создается с оператором TO, указывающим на эту таблицу. Эта функциональность доступна, начиная с dbt-clickhouse версии 1.10. Внимание: эта функция находится в бета-версии, и ее API может измениться на основе отзывов сообщества.Beta

Выбранный вами подход влияет на то, как обрабатываются изменения схемы, полные обновления (full refresh) и конфигурации с несколькими MV. В следующих разделах каждый подход описан более подробно.

Материализация с неявной целевой таблицей

Это поведение по умолчанию. Когда вы определяете модель materialized_view, адаптер будет:

  1. Создавать целевую таблицу с именем модели
  2. Создавать в ClickHouse materialized view с именем <model_name>_mv

Схема целевой таблицы выводится из столбцов в операторе SELECT соответствующей materialized view (MV). Все ресурсы (целевая таблица и все materialized view) используют одну и ту же конфигурацию модели.

-- models/events_mv.sql
{{
    config(
        materialized='materialized_view',
        engine='SummingMergeTree()',
        order_by='(event_date, event_type)'
    )
}}

SELECT
    toStartOfDay(event_time) AS event_date,
    event_type,
    count() AS total
FROM {{ source('raw', 'events') }}
GROUP BY event_date, event_type

Дополнительные примеры см. в тестовом файле.

Несколько materialized view

ClickHouse позволяет нескольким materialized view записывать данные в одну и ту же целевую таблицу. Чтобы поддержать это в dbt-clickhouse при использовании подхода с неявной целевой таблицей, вы можете построить UNION в файле модели, оборачивая SQL для каждого materialized view комментариями вида --my_mv_name:begin и --my_mv_name:end.

Например, следующий пример создаст два materialized view, которые оба записывают данные в одну и ту же целевую таблицу модели. Имена этих materialized view будут иметь вид <model_name>_mv1 и <model_name>_mv2:

--mv1:begin
select a,b,c from {{ source('raw', 'table_1') }}
--mv1:end
union all
--mv2:begin
select a,b,c from {{ source('raw', 'table_2') }}
--mv2:end

ВАЖНО!

При обновлении модели с несколькими materialized view (MV), особенно при переименовании одной из MV, dbt-clickhouse не удаляет старую MV автоматически. Вместо этого вы получите следующее предупреждение: Warning - Table <previous table name> was detected with the same pattern as model name <your model name> but was not found in this run. In case it is a renamed mv that was previously part of this model, drop it manually (!!!)

Как управлять обходом схемы целевой таблицы

Начиная с dbt-clickhouse версии 1.9.8, вы можете управлять тем, как выполняется обход схемы целевой таблицы, когда dbt run обнаруживает разные столбцы в SQL материализованного представления (MV).

{{config(
    materialized='materialized_view',
    engine='MergeTree()',
    order_by='(id)',
    on_schema_change='fail'  # this setting
)}}

По умолчанию dbt не будет применять какие-либо изменения к целевой таблице (значение настройки ignore), но вы можете изменить эту настройку, чтобы поведение соответствовало конфигурации on_schema_change в инкрементальных моделях.

Кроме того, вы можете использовать эту настройку как дополнительный механизм защиты. Если вы установите её в значение fail, сборка завершится с ошибкой, если столбцы в SQL материализованного представления (MV) отличаются от целевой таблицы, которая была создана при первом выполнении dbt run.

Дозагрузка данных

По умолчанию при создании или пересоздании materialized view (MV) целевая таблица сначала заполняется историческими данными до создания самой MV (catchup=True). Это поведение можно отключить, установив параметр catchup в значение False.

{{config(
    materialized='materialized_view',
    engine='MergeTree()',
    order_by='(id)',
    catchup=False  # this setting
)}}
Operationcatchup: True (по умолчанию)catchup: False
Initial deployment (dbt run)Целевая таблица заполняется историческими даннымиЦелевая таблица создаётся пустой
Full refresh (dbt run --full-refresh)Целевая таблица перестраивается и заполняется зановоЦелевая таблица пересоздаётся пустой, существующие данные теряются
Normal operationmaterialized view фиксирует новые вставкиmaterialized view фиксирует новые вставки
Риск потери данных при полном обновлении

Использование catchup: False с dbt run --full-refresh приведёт к тому, что все существующие данные в целевой таблице будут удалены. Таблица будет пересоздана пустой и далее будет содержать только новые данные. Убедитесь, что у вас есть резервные копии, если исторические данные могут понадобиться позже.

Материализация с явной целевой таблицей (Beta)

Beta

Эта функция находится на стадии бета-тестирования и доступна, начиная с dbt-clickhouse версии 1.10. API может измениться в зависимости от отзывов сообщества.

По умолчанию dbt-clickhouse создаёт и управляет как целевой таблицей, так и materialized view в рамках одной модели (подход implicit target, описанный выше). У этого подхода есть несколько ограничений:

  • Все ресурсы (целевая таблица + MV) используют одну и ту же конфигурацию. Если несколько MV указывают на одну и ту же целевую таблицу, они должны быть определены вместе с использованием синтаксиса UNION ALL.
  • Все эти ресурсы нельзя обрабатывать по отдельности — ими нужно управлять через один и тот же файл модели.
  • Вы не можете гибко управлять именем каждой MV.
  • Все настройки общие для целевой таблицы и MV, что затрудняет индивидуальную конфигурацию каждого ресурса и понимание того, какая конфигурация относится к какому ресурсу.

Функция explicit target позволяет определить целевую таблицу отдельно как обычную материализацию table, а затем ссылаться на неё из ваших моделей materialized view.

Преимущества

  • Полностью разделённые ресурсы: теперь каждый ресурс может определяться отдельно, что улучшает читаемость.
  • Соответствие ресурсов dbt и CH 1:1: теперь вы можете использовать инструменты dbt, чтобы независимо управлять этими ресурсами и итеративно их развивать.
  • Доступны разные конфигурации: теперь к каждому ресурсу можно применять собственную конфигурацию.
  • Больше нет необходимости соблюдать соглашения об именовании: теперь все ресурсы создаются с именем, которое задаёт пользователь, а не с добавленным суффиксом _mv для MVs.

Ограничения

  • Определение целевой таблицы не является естественным для dbt: это не SQL, который читает из исходной таблицы, поэтому здесь мы теряем проверки dbt. SQL материализованного представления по‑прежнему будет проверяться с использованием утилит dbt, а его совместимость со столбцами целевой таблицы будет проверяться на уровне ClickHouse.
  • Мы выявили некоторые проблемы, связанные с ограничениями функции ref(): нам нужно использовать её для ссылок между моделями, но её можно использовать только для ссылок на вышестоящие модели, а не на нижестоящие. Это создаёт определённые проблемы для данной реализации. Мы создали issue в репозитории dbt-core и сейчас обсуждаем с ними поиск возможных решений (dbt-labs/dbt-core#12319):
    • Когда ref() вызывается изнутри блока config, она возвращает текущую модель, а не общую. Это не позволяет нам определять её в секции config(), вынуждая использовать комментарий для добавления этой зависимости. Мы следуем тому же подходу, который описан в документации dbt, с подходом "--depends_on:".
    • ref() работает для нас, поскольку она заставляет сначала создать целевую таблицу, но на графе зависимостей в сгенерированной документации целевая таблица будет показана как ещё одна вышестоящая зависимость, а не нижестоящая, что несколько усложняет понимание.
    • unit-test также вынуждает нас определять некоторые данные для целевой таблицы, даже если задумка состоит в том, чтобы не читать из неё. Обходной путь — просто оставить данные для этой таблицы пустыми.

Использование

Шаг 1. Определите целевую таблицу как обычную модель таблицы

Модель events_daily.sql:

{{
    config(
        materialized='table',
        engine='SummingMergeTree()',
        order_by='(event_date, event_type)',
        partition_by='toYYYYMM(event_date)'
    )
}}

SELECT
    toDate(now()) AS event_date,
    '' AS event_type,
    toUInt64(0) AS total
WHERE 0  -- Creates empty table with correct schema

Это обходной вариант, который мы упоминаем в разделе с ограничениями. Здесь вы можете лишиться части проверок dbt, но схема по‑прежнему будет проверяться на уровне ClickHouse.

Шаг 2: Определите materialized views, указывающие на целевую таблицу

Например, вы можете определить разные MV в разных моделях следующим образом, даже если они указывают на одну и ту же целевую таблицу. Обратите внимание на новый вызов макроса {{ materialization_target_table(ref('events_daily')) }}, который настраивает целевую таблицу для MV.

Модель page_events_aggregator.sql:

{{ config(materialized='materialized_view') }}
{{ materialization_target_table(ref('events_daily')) }}

SELECT
    toStartOfDay(event_time) AS event_date,
    event_type,
    count() AS total
FROM {{ source('raw', 'page_events') }}
GROUP BY event_date, event_type

Модель mobile_events_aggregator.sql:

{{ config(materialized='materialized_view') }}
{{ materialization_target_table(ref('events_daily')) }}

SELECT
    toStartOfDay(event_time) AS event_date,
    event_type,
    count() AS total
FROM {{ source('raw', 'mobile_events') }}
GROUP BY event_date, event_type

Параметры конфигурации

При использовании явных целевых таблиц доступны следующие параметры конфигурации:

Для целевой таблицы (materialized='table'):

OptionDescriptionDefault
mv_on_schema_changeКак обрабатывать изменения схемы, когда таблица используется materialized view под управлением dbt. Поведение соответствует параметру конфигурации on_schema_change в инкрементальных моделях.Внимание: Модель materialized='table' будет вести себя как обычно, если к ней не привязаны materialized view, поэтому, даже если этот параметр задан, он будет проигнорирован. Если таблица является целевой для materialized view, этот параметр по умолчанию будет иметь значение mv_on_schema_change='fail', чтобы защитить данные в этих таблицах.
repopulate_from_mvs_on_full_refreshПри --full-refresh вместо выполнения SQL таблицы перестраивать таблицу, выполняя INSERT-SELECT на основе SQL всех materialized view, которые на неё ссылаются.False

Для materialized view (materialized='materialized_view'):

OptionDescriptionDefault
catchupНужно ли заполнять исторические данные при создании materialized view.True
Примечание

Обычно имеет смысл устанавливать catchup в True только в materialized view или repopulate_from_mvs_on_full_refresh в True только в их целевых таблицах. Если установить оба параметра в True, это может привести к дублированию данных.

Распространённые операции

Полное обновление с явными целевыми таблицами

При использовании --full-refresh явные целевые таблицы будут пересозданы (поэтому есть риск потери данных, если приём данных происходит во время этого процесса). Поведение будет отличаться в зависимости от ваших настроек:

Вариант 1: поведение --full-refresh по умолчанию. Всё пересоздаётся, но во время пересоздания материализованных представлений (MV) целевая таблица будет пустой или частично загруженной.

Всё удаляется и пересоздаётся. Если вы хотите повторно вставить данные с помощью SQL материализованных представлений (MV), оставьте настройку catchup=True:

-- models/page_events_aggregator.sql
{{ config(
    materialized='materialized_view',
    catchup=True  -- this is the default value so you don't need to actully set it.
) }}
{{ materialization_target_table(ref('events_daily')) }}
...

Вариант 2: я хочу пересоздать целевую таблицу и не хочу, чтобы при пересоздании MV читались пустые данные.

Если вам сначала нужно обновить SQL определений MV, задайте в них catchup=False, а затем выполните dbt run или dbt run --full-refresh для MV. Убедитесь, что MV созданы до запуска --full-refresh для целевой таблицы, так как при этом используются определения MV из ClickHouse.

Установите repopulate_from_mvs_on_full_refresh=True в модели целевой таблицы. При выполнении dbt run --full-refresh это приведёт к следующему:

  1. Будет создана новая временная таблица
  2. Будет выполнен INSERT-SELECT с использованием SQL каждого MV
  3. Таблицы будут атомарно поменяны местами

Таким образом, пользователи вашей таблицы не столкнутся с пустыми данными, пока MV пересоздаются.

-- models/events_daily.sql
{{
    config(
        materialized='table',
        engine='SummingMergeTree()',
        order_by='(event_date, event_type)',
        repopulate_from_mvs_on_full_refresh=True
    )
}}
...

Изменение целевой таблицы

Нельзя изменить целевую таблицу материализованного представления (MV) без выполнения --full-refresh. Если вы попытаетесь запустить обычный dbt run после изменения ссылки в materialization_target_table(), сборка завершится с ошибкой с сообщением о том, что целевая таблица изменилась.

Чтобы изменить целевую таблицу:

  1. Обновите вызов materialization_target_table()
  2. Выполните dbt run --full-refresh -s your_mv_model

Сравнение поведения между подходами с неявной и явной целевой таблицей

OperationImplicit targetExplicit target
First dbt runВсе ресурсы созданыВсе ресурсы созданы
Next dbt runОтдельными ресурсами нельзя управлять независимо, все операции выполняются вместе:

target table:
изменения обрабатываются с помощью настройки on_schema_change. По умолчанию установлено значение ignore, поэтому новые столбцы не обрабатываются.

MVs: все обновляются с помощью операций alter table modify query
Изменения могут применяться по отдельности:

target table
:
автоматическое определение того, являются ли они целевыми таблицами для MVs, определённых в dbt. Если да, то изменения структуры столбцов по умолчанию управляются с помощью настройки mv_on_schema_change со значением fail, поэтому выполнение завершится ошибкой при изменении столбцов. Мы добавили это значение по умолчанию как дополнительный уровень защиты

MVs: их SQL обновляется операциями alter table modify query.
dbt run --full-refreshОтдельными ресурсами нельзя управлять независимо, все операции выполняются вместе:

target table
:
target table пересоздаётся пустой. Параметр catchup позволяет настроить backfill с использованием объединённого SQL всех MVs. По умолчанию catchup имеет значение True

MVs: все пересоздаются.
Изменения будут применяться по отдельности:

target table:
будет пересоздана как обычно.

MVs: удаляются и пересоздаются. Параметр catchup доступен для начального backfill. По умолчанию catchup имеет значение True.

Примечание: В процессе выполнения target table будет пустой или частично загруженной до тех пор, пока MVs не будут пересозданы. Чтобы этого избежать, см. следующий раздел о том, как поэтапно обновлять целевую таблицу.

Миграция от неявной к явной целевой таблице

Если у вас есть существующие модели materialized view, использующие подход с неявной целевой таблицей, и вы хотите перейти на подход с явной целевой таблицей, выполните следующие шаги:

1. Создайте модель целевой таблицы

Создайте новый файл модели с materialized='table', который задаёт ту же схему, что и текущая целевая таблица MV. Используйте предложение WHERE 0, чтобы создать пустую таблицу. Используйте то же имя, что и у текущей модели неявного materialized view. Теперь вы сможете использовать эту модель для итеративного изменения целевой таблицы.

-- models/events_daily.sql
{{
    config(
        materialized='table',
        engine='MergeTree()',
        order_by='(event_date, event_type)'
    )
}}

SELECT
    toDate(now()) AS event_date,
    '' AS event_type,
    toUInt64(0) AS total
WHERE 0

2. Обновите MV‑модели

Создайте новые модели, которые будут включать SQL‑код MV и вызов макроса materialization_target_table(), указывающий на новую целевую таблицу. Если вы ранее использовали UNION ALL, удалите эту часть и комментарии.

Для имён моделей необходимо придерживаться следующего соглашения об именовании:

  • если было определено только одно MV, оно будет иметь имя: <old_model_name>_mv
  • если было определено несколько MV, каждое будет иметь имя: <old_model_name>_mv_<name_in_comments>

Ранее в my_model.sql (неявная целевая таблица, одна модель с UNION ALL):

--mv1:begin
select a, b, c from {{ source('raw', 'table_1') }}
--mv1:end
union all
--mv2:begin
select a, b, c from {{ source('raw', 'table_2') }}
--mv2:end

После (явно заданная цель, отдельные файлы моделей):

-- models/my_model_mv_mv1.sql
{{ config(materialized='materialized_view') }}
{{ materialization_target_table(ref('events_daily')) }}

select a, b, c from {{ source('raw', 'table_1') }}
-- models/my_model_mv_mv2.sql
{{ config(materialized='materialized_view') }}
{{ materialization_target_table(ref('events_daily')) }}

select a, b, c from {{ source('raw', 'table_2') }}

3. При необходимости повторяйте их, руководствуясь инструкциями из раздела «Явная цель».

Поведение во время активной ингестии

Поскольку materialized view в ClickHouse действуют как триггеры на вставку (insert triggers), они обрабатывают данные только пока существуют. Если materialized view удаляется и создаётся заново (например, во время --full-refresh), любые строки, вставленные в исходную таблицу в этот промежуток времени, не будут обработаны этой MV. Такое состояние MV называют «слепым» (blind).

Кроме того, процесс catch-up (как через catchup у самой MV, так и через repopulate_from_mvs_on_full_refresh целевой таблицы) выполняет INSERT INTO ... SELECT, используя SQL самой materialized view. Если вставки в исходную таблицу происходят одновременно, запрос catch-up может включить строки, которые MV уже обработала (или обработает сразу после создания), что потенциально приводит к появлению дубликатов данных в целевой таблице. Использование дедуплицирующего движка, такого как ReplacingMergeTree, для целевой таблицы снижает этот риск.

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

Неявные операции с целевой таблицей

OperationInternal processSafety while inserts are happening
Первый dbt run1. Создать целевую таблицу
2. Вставить данные (если catchup=True)
3. Создать MV(ы)
⚠️ MV «ничего не видит» между шагами 1 и 3. Любые строки, вставленные в исходную таблицу в этот промежуток, не будут зафиксированы.
Последующие dbt runALTER TABLE ... MODIFY QUERY✅ Безопасно. MV обновляется атомарно.
dbt run --full-refresh1. Создать резервную таблицу
2. Вставить данные (если catchup=True)
3. Удалить MV(ы)
4. Поменять таблицы местами
5. Воссоздать MV(ы)
⚠️ MV «ничего не видит» во время пересоздания. Данные, вставленные в исходную таблицу между шагами 3 и 5, не попадут в новую целевую таблицу.

Явные операции с целевыми объектами

Модели materialized view:

OperationInternal processSafety while inserts are happening
First dbt run1. Создать MV (с TO-клаузой)
2. Выполнить догоняющее заполнение (если catchup=True)
✅ MV создаётся первой, поэтому новые вставки сразу же попадают в неё.
⚠️ Догоняющее заполнение может дублировать данные — запрос backfill может пересекаться со строками, которые уже обрабатываются MV. Безопасно при использовании дедуплицирующего движка (например, ReplacingMergeTree).
Subsequent dbt runALTER TABLE ... MODIFY QUERY✅ Безопасно. MV обновляется атомарно.
dbt run --full-refresh на MV1. Удалить и пересоздать MV
2. Выполнить догоняющее заполнение (если catchup=True)
⚠️ MV "слепа" во время пересоздания (между удалением и созданием).
⚠️ Догоняющее заполнение может дублировать данные, если вставки выполняются одновременно.

Модель целевой таблицы:

OperationInternal processSafety while inserts are happening
dbt runИзменения схемы применяются согласно настройке mv_on_schema_change✅ Безопасно. Перемещения данных не происходит.
dbt run --full-refresh (по умолчанию)Пересоздать таблицу (оставив её пустой)⚠️ Целевая таблица пуста, пока MV не заполнят её в ходе backfill-а. MV продолжают вставлять данные в новую таблицу, как только она появляется.
dbt run --full-refresh с repopulate_from_mvs_on_full_refresh=True1. Создать резервную таблицу
2. Вставить данные, используя SQL каждой MV
3. Атомарно поменять таблицы местами
⚠️ MV "слепа" во время пересоздания. Данные, вставленные между шагами 1 и 3, не появятся в новой таблице. Это может измениться в следующих версиях
Рекомендации для продакшен-сред с активной ингестией
  • По возможности приостанавливайте приём данных во время операций dbt: это сделает все операции безопасными и исключит потерю данных.
  • По возможности используйте дедуплицирующий движок (например, ReplacingMergeTree) на целевой таблице для обработки потенциальных дубликатов из-за пересечения догоняющего заполнения.
  • Отдавайте предпочтение ALTER TABLE ... MODIFY QUERY (обычный dbt run без --full-refresh), когда это возможно — это всегда безопасно.
  • Учитывайте проблемные интервалы времени во время операций dbt.

Обновляемые materialized view

Refreshable Materialized Views — это особый тип materialized view в ClickHouse, которые периодически повторно выполняют запрос и сохраняют результат, аналогично тому, как materialized view работают в других базах данных. Это полезно в сценариях, когда нужны периодические срезы или агрегации, а не триггеры вставки в режиме реального времени.

Совет

Refreshable materialized view могут использоваться как с неявной целевой таблицей, так и с явной целевой таблицей. Конфигурация refreshable не зависит от того, как управляется целевая таблица.

Чтобы использовать refreshable materialized view, добавьте объект конфигурации refreshable в вашу MV-модель со следующими параметрами:

OptionDescriptionRequiredDefault Value
refresh_intervalОбязательное поле с интерваломYes
randomizeПараметр рандомизации, будет добавлен после RANDOMIZE FOR
appendЕсли установлено в True, при каждом обновлении в таблицу вставляются новые строки без удаления существующих. Вставка не является атомарной, так же как обычный INSERT SELECT.False
depends_onСписок зависимостей для refreshable materialized view. Укажите зависимости в следующем формате: {schema}.{view_name}
depends_on_validationНужно ли проверять существование зависимостей, указанных в depends_on. Если зависимость не содержит схемы, проверка выполняется в схеме default.False

Пример с неявно заданной целевой таблицей

{{
    config(
        materialized='materialized_view',
        engine='MergeTree()',
        order_by='(event_date)',
        refreshable={
            "interval": "EVERY 5 MINUTE",
            "randomize": "1 MINUTE",
            "append": True,
            "depends_on": ['schema.depend_on_model'],
            "depends_on_validation": True
        }
    )
}}

SELECT
    toStartOfDay(event_time) AS event_date,
    count() AS total
FROM {{ source('raw', 'events') }}
GROUP BY event_date

Пример с явным указанием целевой таблицы

{{
    config(
        materialized='materialized_view',
        refreshable={
            "interval": "EVERY 1 HOUR",
            "append": False
        }
    )
}}
{{ materialization_target_table(ref('events_daily')) }}

SELECT
    toStartOfDay(event_time) AS event_date,
    event_type,
    count() AS total
FROM {{ source('raw', 'events') }}
GROUP BY event_date, event_type

Ограничения

  • При создании refreshable materialized view (MV) в ClickHouse, у которой есть зависимость, ClickHouse не выдаёт ошибку, если указанная зависимость не существует на момент создания. Вместо этого refreshable MV остаётся в неактивном состоянии, ожидая удовлетворения зависимости, прежде чем начать обрабатывать обновления или выполнять refresh. Такое поведение является ожидаемым, но может привести к задержкам в доступности данных, если требуемая зависимость не будет своевременно обеспечена. Рекомендуется перед созданием refreshable materialized view убедиться, что все зависимости корректно определены и существуют.
  • В настоящее время не существует фактической «dbt-связи» между MV и её зависимостями, поэтому порядок создания не гарантируется.
  • Функциональность refreshable не тестировалась с несколькими MV, направляющими данные в одну и ту же целевую модель.