Skip to main content
Это руководство описывает специфичные для ClickHouse аспекты работы с dbt на примере проекта Jaffle Shop for ClickHouse — порта классического демонстрационного проекта dbt Labs под ClickHouse. Отправной точкой служит уже собирающийся проект, на котором показано, как:
  1. Разобраться, во что превращаются просмотры и таблицы проекта в ClickHouse.
  2. Загружать данные с помощью seed-ов и управлять типами ClickHouse и структурой таблиц.
  3. Настроить табличную модель с указанием движка ClickHouse, ключа сортировки и партиционирования.
  4. Превратить таблицу в инкрементальную модель и выбрать инкрементальную стратегию.
  5. Создать снимок.
  6. Использовать materialized просмотры ClickHouse.
Руководство рассчитано на чтение вместе с остальной документацией, страницей возможностей и конфигураций и справочником по materializations.

Прежде чем начать

Сначала выполните инструкции из README проекта ClickHouse/jaffle-shop-clickhouse. Там объясняется, как настроить проект с dbt Core 1.x, dbt OSS, dbt v2 или платформой dbt, как подключить его к локальному ClickHouse (docker) или ClickHouse Cloud, как загрузить демонстрационные данные командой dbt seed и как выполнить первый dbt build. После того как dbt build успешно завершится, возвращайтесь сюда за примерами и конфигурациями, специфичными для ClickHouse. После выполнения шагов из README в ClickHouse у вас должно быть две базы данных:
  • raw: шесть исходных таблиц, загруженных из CSV-файлов командой dbt seed (raw_customers, raw_orders, raw_items, raw_products, raw_stores, raw_supplies).
  • jaffle_shop (значение schema в вашем профиле): шесть промежуточных просмотров (stg_*) и семь витринных таблиц (customers, orders, order_items, products, locations, supplies, metricflow_time_spine).
Если в вашем профиле используется другая schema, замените jaffle_shop в приведённых ниже запросах на своё значение.
dbt Core 1.x, dbt OSS, dbt v2 и платформа dbt. Все команды и модели в этом руководстве одинаковы для всех них. Примеры проверялись на dbt Core 1.12 с dbt-clickhouse 1.10 и на dbt OSS 2.0 с ClickHouse 26.8; dbt v2 использует тот же adapter, а платформа dbt работает на dbt v2. Приведённый вывод в консоли получен в dbt Core 1.x; те немногие случаи, где поведение движков различается, отмечены отдельно. Текущий статус адаптера v2 см. на странице dbt OSS, dbt v2 и платформы dbt, а для начала работы с платформой dbt — раздел Connect ClickHouse в документации dbt.
Все SQL-команды, не являющиеся командами dbt, предназначены для выполнения непосредственно в ClickHouse — например, с помощью clickhouse client, SQL-консоли ClickHouse Cloud или любого другого SQL-клиента на ваш выбор.

Как материализуется проект

В Jaffle Shop materializations задаются в dbt_project.yml: staging-модели создаются как просмотры, а marts — как таблицы.
Модель типа view пересоздаётся оператором CREATE OR REPLACE VIEW при каждом запуске. Она не хранит данные, поэтому её построение ничего не стоит, но при каждом запросе к ней SQL модели выполняется по исходным таблицам. ClickHouse хранит скомпилированный SQL модели в определении представления:
Модель типа table пересоздаётся с нуля при каждом запуске: adapter создаёт новую таблицу, выполняет INSERT INTO ... SELECT с SQL-кодом модели и атомарно меняет её местами с предыдущей версией. Производительность запросов существенно выше, чем у представления, но платой за это становятся расходы на хранилище и полное пересоздание таблицы каждый раз. Посмотрите на таблицу, которую dbt создал для витрины orders:
Здесь есть два момента, специфичных для ClickHouse. В модели не указан движок таблицы, поэтому adapter использует MergeTree, и не указан ключ сортировки, поэтому adapter применяет ORDER BY tuple() — то есть данные не сортируются вовсе. Для демонстрационного проекта это приемлемо, но для реальной таблицы стоит задать и то, и другое, чем мы и займёмся в следующих разделах. На странице materializations перечислены все конфигурации таблиц, которые поддерживает adapter.

Загрузка данных с помощью seeds

Jaffle Shop использует dbt seeds для загрузки необработанных данных из CSV-файлов в каталоге seeds/jaffle-data. Seeds предназначены для небольших статических справочных данных (таблицы кодов, соответствия), а не для наполнения хранилища; в проекте они используются для удобства, чтобы можно было начать работу без дополнительного инструмента ингестии — именно поэтому seeds отключены, пока вы не передадите --vars '{"load_source_data": true}'. Тем не менее seeds — хорошая отправная точка, чтобы разобраться, как dbt создаёт таблицы ClickHouse. dbt выводит тип столбца для каждого столбца CSV, и выведенные типы различаются в зависимости от движка: Если тип важен, задайте его явно через column_types. В проекте это уже сделано для столбца opened_at seed-а raw_stores в файле dbt_project.yml:
Seeds также поддерживают конфигурации таблиц ClickHouse: engine, order_by и partition_by. Например, чтобы отсортировать seed raw_orders по времени заказа и разбить его на партиции по месяцам, добавьте рядом с CSV-файлами файл свойств seeds/jaffle-data/_raw_orders.yml:
Для этих seed-конфигураций ClickHouse используйте файл свойств, а не ключи +order_by или +engine в разделе seeds: файла dbt_project.yml. dbt Core 1.x принимает оба варианта, однако dbt v2 распознаёт их только в файле свойств и отклоняет ключи из dbt_project.yml с ошибкой Unrecognized key ... Custom keys must go under +meta.
Загрузите этот seed заново и проверьте созданную им таблицу:
dbt seed --full-refresh удаляет и заново создаёт таблицу, поэтому выполните эту команду до создания любых объектов, которые напрямую зависят от данных этой таблицы, — например, materialized view, рассматриваемого далее в этом руководстве.

Настройка таблицы для ClickHouse

Витрина orders — естественная точка, с которой стоит начать: к ней обращаются витрина customers и метрики проекта, к тому же это таблица событийного типа с временной меткой. Добавьте блок config в начало файла models/marts/orders.sql, чтобы задать движок, ключ сортировки и схему партиционирования:
Остальная часть модели остаётся без изменений. materialized='table' дублирует то, что уже задано в dbt_project.yml для marts, благодаря чему модель остаётся самоописывающейся, когда вы позже переведёте её в инкрементальный режим. Пересоберите только эту модель:
Теперь у таблицы есть корректный ключ сортировки и по одной партиции на каждый месяц:
Помимо engine, order_by и partition_by, модели типа table принимают primary_key, ttl, settings, query_settings, projections и indexes, а столбцы могут получать codec и ttl через контракт модели. Все они описаны на странице materializations.

Создание инкрементальной модели

Полная пересборка orders при каждом запуске вполне приемлема для 62 000 строк, но не для таблицы, которая растёт на миллионы строк в день. Инкрементальная материализация в dbt обрабатывает только те строки, которые изменились с момента последнего запуска. Чтобы преобразовать модель orders, нужно добавить два элемента:
  1. unique_key: столбец, идентифицирующий строку — в данном случае order_id. Adapter использует его, чтобы заменять повторно обработанные строки, а не дублировать их.
  2. Инкрементальный фильтр: предложение where, обёрнутое в {% if is_incremental() %}, которое отбирает только строки, подлежащие обработке. Оно применяется при инкрементальных запусках, но не при первом построении таблицы (или при пересборке с --full-refresh). Заказы содержат временную метку, поэтому фильтр сравнивает ordered_at с последним значением, уже имеющимся в таблице, обращаясь к ней через переменную {{ this }}.
Обновите models/marts/orders.sql так, чтобы блок config и конец модели выглядели следующим образом:
stg_orders усекает ordered_at до дня, поэтому в фильтре используется >=: при каждом запуске весь последний день обрабатывается заново, а благодаря unique_key уже загруженные строки заменяются, а не дублируются. Именно поэтому такой подход безопасен для заказов, поступающих позже в течение того же дня. Запустите модель. Таблица уже существует, поэтому этот первый запуск сразу будет инкрементальным: повторно обрабатывается только последний день.
Теперь добавим новые данные. Данные Jaffle Shop заканчиваются августом 2025 года, поэтому введём нового клиента — Clicky McClickHouse, который вчера заказал джаффл. Вставим клиента, заказ и позицию заказа в исходные (raw) таблицы:
Идентификатор магазина — Philadelphia, товар — jaffle nutellaphone who dis? за 11.00, налог — 6%, как в Филадельфии, поэтому тесты данных проекта по-прежнему проходят. Запустите проект целиком, чтобы промежуточные представления и таблица order_items увидели новые строки раньше, чем orders:
Новый заказ попал в инкрементальную таблицу, а витрина customers, перестроенная на её основе, уже знает о новом клиенте:

Внутреннее устройство

В журнале запросов ClickHouse видны команды, которые adapter выполнил для инкрементального обновления:
Инкрементальная стратегия адаптера по умолчанию работает следующим образом. На схемах в этом разделе стрелка от таблицы к оператору означает, что оператор читает эту таблицу; стрелка от оператора к таблице означает, что он пишет в неё, изменяет, переименовывает или удаляет её:
  1. Создаётся таблица orders__dbt_new_data, и в неё вставляется результат SQL-запроса модели, включая инкрементальный фильтр. В приведённом выше запуске было записано 378 строк: 377 заказов последнего дня, загруженных ранее, плюс один новый.
  2. Создаётся таблица orders__dbt_tmp с той же структурой, что и orders, и в неё копируются все строки orders, у которых order_id отсутствует в orders__dbt_new_data.
  3. Все строки orders__dbt_new_data вставляются в orders__dbt_tmp. Именно шаги 2 и 3 обеспечивают замену строк последнего дня вместо их дублирования.
  4. Таблица orders__dbt_new_data удаляется.
  5. orders__dbt_tmp меняется местами с orders с помощью атомарного оператора EXCHANGE TABLES (через промежуточное переименование в orders__dbt_backup), так что теперь orders содержит новую версию.
  6. Старая версия удаляется.
На шаге 2 таблица копируется целиком, поэтому на очень больших моделях эта стратегия обходится так же дорого, как полное пересоздание таблицы; см. ограничения. Описанные ниже стратегии позволяют избежать копирования.

Append strategy

Стратегия append вставляет строки, отобранные моделью, напрямую в целевую таблицу. Временные таблицы не создаются, ничего не копируется — это самый дешёвый из возможных инкрементальных запусков. Плата за это — отсутствие дедупликации: если инкрементальный фильтр отберёт строку, которая уже есть в таблице, она попадёт туда во второй раз. Используйте эту стратегию для неизменяемых данных событийного характера и следите за тем, чтобы фильтр отбирал только действительно новые строки. С усечённым до дня ordered_at это означает замену условия фильтра на >. Измените модель:
Добавьте второго нового покупателя, Danny DeBito, с заказом, оформленным сегодня в Бруклине (налог 4%), содержащим jaffle и кофе:
Инкрементальная модель отработала за долю времени, которое потребовалось при предыдущем запуске. У обоих новых клиентов в таблице ровно по одному заказу:
Журнал запросов подтверждает разность: на этот раз единственный оператор, затрагивающий orders, — это один INSERT INTO jaffle_shop.orders ... SELECT ... с SQL модели и инкрементальным фильтром, и он записал одну строку.
При использовании > и временной метки, усечённой до дня, заказ, поступивший позже в тот же день, что и последний загруженный заказ, так и не будет учтён. В реальном проекте при использовании стратегии append фильтруйте по временной метке с полной точностью или по монотонно возрастающему времени ингестии.

Стратегия delete и insert

Исторически ClickHouse поддерживал обновления и удаления лишь ограниченно — в виде асинхронных мутаций. Они могут создавать крайне высокую нагрузку на ввод-вывод, поэтому их, как правило, следует избегать. В ClickHouse 22.8 появились легковесные удаления, а в ClickHouse 25.7 — легковесные обновления. Благодаря им результат отдельного оператора удаления или обновления виден пользователю сразу, хотя физически изменения применяются асинхронно. Стратегия delete+insert основана на легковесных удалениях и настраивается через параметр incremental_strategy:
Она работает напрямую с целевой таблицей, поэтому, если что-то прервётся на полпути, данные в инкрементальной модели, скорее всего, окажутся в некорректном состоянии: атомарной подмены таблиц здесь нет. Итого:
  1. Создаётся временная таблица (orders__dbt_new_data_<run_id>), и в неё вставляются строки, выбранные моделью.
  2. В таблице orders выполняется DELETE для каждого order_id, присутствующего во временной таблице.
  3. Строки из временной таблицы вставляются в orders.
  4. Временная таблица удаляется.

Стратегия insert overwrite (экспериментальная)

Стратегия insert_overwrite заменяет партиции целиком, поэтому ей требуется конфигурация partition_by — например, помесячная, как у orders. Она выполняет следующие шаги:
  1. Создаёт staging-таблицу (orders__dbt_new_data_<run_id>) с той же структурой, что и orders.
  2. Вставляет в staging-таблицу только те строки, которые выбраны моделью.
  3. Получает из system.parts список партиций, присутствующих в staging-таблице.
  4. Заменяет ровно эти партиции в orders командой ALTER TABLE ... REPLACE PARTITION ... FROM из staging-таблицы.
  5. Удаляет staging-таблицу.
У этого подхода есть следующие преимущества:
  • Он быстрее стратегии по умолчанию, поскольку не копирует таблицу целиком.
  • Он безопаснее остальных стратегий, поскольку не изменяет исходную таблицу, пока операция INSERT не завершится успешно: при промежуточном сбое исходная таблица остаётся неизменной.
  • Он реализует принцип «неизменяемости партиций» — лучшую практику инженерии данных, которая упрощает инкрементальную и параллельную обработку данных, откаты и т. д.
На странице materializations рассмотрены остальные параметры инкрементальной материализации, включая стратегию microbatch и on_schema_change.

Создание снимка

Снимки dbt фиксируют, как строки изменяемой таблицы меняются со временем, что позволяет аналитикам увидеть состояние данных на любой момент в прошлом. Они реализуют медленно меняющиеся измерения второго типа: каждая версия строки сохраняется вместе с интервалом, в течение которого она была актуальна. Витрина customers подходит для этого как нельзя лучше: значения count_lifetime_orders, lifetime_spend и customer_type меняются при каждом новом заказе клиента. Прежде чем продолжить, верните модель orders к инкрементальной стратегии по умолчанию из раздела об инкрементальных моделях (удалите incremental_strategy='append' и измените фильтр обратно на >=), чтобы заказы, оформленные позже в течение текущего дня, тоже попадали в выборку. Начиная с dbt 1.9 снимки определяются в YAML. Создайте snapshots/customers_snapshot.yml:
Стратегия check при каждом запуске сравнивает перечисленные столбцы в текущем снимке и в источнике и записывает новую версию, если любой из них изменился. Если в вашей модели есть надёжный столбец временной метки «последнего обновления», стратегия timestamp обойдётся дешевле: укажите strategy: timestamp и updated_at: <column>. В Jaffle Shop значение last_ordered_at усечено до дня, поэтому второй заказ в тот же день остался бы незамеченным — именно поэтому в данном примере используется check. Создайте первый снимок:
Таблица снимка создаётся рядом с models. Macro generate_schema_name этого проекта помещает каждое отношение в целевую схему для непродакшн-целей, поэтому config schema у снимка вступит в силу только с целью prod. Она содержит по одной строке на каждого клиента, а также служебные столбцы dbt dbt_valid_from и dbt_valid_to; последний равен NULL для текущей версии строки:
Сегодня Clicky снова заходит на кофе:
Запустите models, чтобы в orders и customers отразился новый заказ, а затем создайте второй снимок:
Теперь в снимке для Clicky две строки. Первая версия закрыта — у неё проставлено значение dbt_valid_to, а новая версия, где клиент уже имеет статус returning и два заказа, остаётся открытой. Данные по Danny не изменились, поэтому его строка осталась прежней:
Под капотом adapter собирает новую версию снимка в таблице customers_snapshot__snapshot_upsert и подменяет ею текущую с помощью EXCHANGE TABLES (либо через drop и rename, если server не поддерживает обмен таблицами), так что считыватели видят либо предыдущую, либо новую версию снимка. Справочник по configuration см. в разделе о snapshot на странице materializations.

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

Всё, что рассматривалось до сих пор, требует выполнения dbt run, чтобы новые данные попали в модели. В ClickHouse materialized view работают иначе: это триггеры вставки. Каждый блок строк, вставленный в исходную таблицу, преобразуется запросом SELECT этого представления и записывается в целевую таблицу — без какого-либо расписания. Адаптер предоставляет их через материализацию materialized_view. Создайте файл models/marts/daily_store_revenue.sql с количеством заказов и выручкой по каждому магазину за день, читающий данные напрямую из сырой таблицы заказов:
Параметры engine и order_by применяются к целевой таблице. При слиянии частей SummingMergeTree суммирует числовые столбцы строк с одинаковым ключом сортировки — именно это и требуется для агрегата по дням и магазинам.
Адаптер создал два объекта: целевую таблицу, названную по имени модели, и сам materialized view с суффиксом _mv, который ссылается на целевую таблицу через клаузу TO. По умолчанию (catchup=True) в целевую таблицу также была выполнена дозагрузка существующих заказов:
Теперь вставьте ещё один «сырой» заказ для Danny, не запуская после этого dbt:
Целевая таблица уже это отражает: теперь у Brooklyn два заказа за сегодня:
Запрос намеренно агрегирует с помощью sum() и GROUP BY: SummingMergeTree сворачивает строки с одинаковым ключом только при фоновом слиянии частей, поэтому до этого момента два заказа из Бруклина остаются двумя строками в таблице. При работе с суммирующими и агрегирующими движками всегда агрегируйте при чтении (или используйте FINAL). При этом в инкрементальной модели orders для Danny по-прежнему числится один заказ — до следующего dbt run. Последующие запуски dbt run сохраняют целевую таблицу и её данные и лишь обновляют определение представления — через ALTER TABLE ... MODIFY QUERY, если изменение это допускает, поэтому модель можно спокойно оставить в проекте. dbt run --full-refresh пересоздаёт целевую таблицу и снова выполняет дозагрузку (если catchup не равно False). Всё остальное описано на странице о materialized view: изменения схемы через on_schema_change, отключение дозагрузки с помощью catchup, refreshable materialized views, несколько представлений, наполняющих одну и ту же целевую таблицу, и определение целевой таблицы как отдельной модели.

Дополнительная информация

Это руководство лишь поверхностно затрагивает dbt. Документация dbt служит справочником по всему, что не относится непосредственно к ClickHouse. Что касается adapterа, см. страницу features and configurations — о настройках профиля и глобальных возможностях, страницу materializations — обо всех использованных выше конфигурациях, а также страницу dbt OSS, dbt v2 и платформы dbt, если вы используете dbt OSS, dbt v2 или платформу dbt. Приветствуется добавление новых примеров в Jaffle Shop for ClickHouse.
Последнее изменение 26 сентября 2026 г.