- Разобраться, во что превращаются просмотры и таблицы проекта в ClickHouse.
- Загружать данные с помощью seed-ов и управлять типами ClickHouse и структурой таблиц.
- Настроить табличную модель с указанием движка ClickHouse, ключа сортировки и партиционирования.
- Превратить таблицу в инкрементальную модель и выбрать инкрементальную стратегию.
- Создать снимок.
- Использовать materialized просмотры ClickHouse.
Прежде чем начать
Сначала выполните инструкции из 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.clickhouse client, SQL-консоли ClickHouse Cloud или любого другого SQL-клиента на ваш выбор.
Как материализуется проект
В Jaffle Shop materializations задаются вdbt_project.yml: staging-модели создаются как просмотры, а marts — как таблицы.
CREATE OR REPLACE VIEW при каждом запуске. Она не хранит данные, поэтому её построение ничего не стоит, но при каждом запросе к ней SQL модели выполняется по исходным таблицам. ClickHouse хранит скомпилированный SQL модели в определении представления:
INSERT INTO ... SELECT с SQL-кодом модели и атомарно меняет её местами с предыдущей версией. Производительность запросов существенно выше, чем у представления, но платой за это становятся расходы на хранилище и полное пересоздание таблицы каждый раз. Посмотрите на таблицу, которую dbt создал для витрины orders:
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:
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.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, нужно добавить два элемента:
unique_key: столбец, идентифицирующий строку — в данном случаеorder_id. Adapter использует его, чтобы заменять повторно обработанные строки, а не дублировать их.- Инкрементальный фильтр: предложение
where, обёрнутое в{% if is_incremental() %}, которое отбирает только строки, подлежащие обработке. Оно применяется при инкрементальных запусках, но не при первом построении таблицы (или при пересборке с--full-refresh). Заказы содержат временную метку, поэтому фильтр сравниваетordered_atс последним значением, уже имеющимся в таблице, обращаясь к ней через переменную{{ this }}.
models/marts/orders.sql так, чтобы блок config и конец модели выглядели следующим образом:
stg_orders усекает ordered_at до дня, поэтому в фильтре используется >=: при каждом запуске весь последний день обрабатывается заново, а благодаря unique_key уже загруженные строки заменяются, а не дублируются. Именно поэтому такой подход безопасен для заказов, поступающих позже в течение того же дня.
Запустите модель. Таблица уже существует, поэтому этот первый запуск сразу будет инкрементальным: повторно обрабатывается только последний день.
nutellaphone who dis? за 11.00, налог — 6%, как в Филадельфии, поэтому тесты данных проекта по-прежнему проходят. Запустите проект целиком, чтобы промежуточные представления и таблица order_items увидели новые строки раньше, чем orders:
customers, перестроенная на её основе, уже знает о новом клиенте:
Внутреннее устройство
В журнале запросов ClickHouse видны команды, которые adapter выполнил для инкрементального обновления:- Создаётся таблица
orders__dbt_new_data, и в неё вставляется результат SQL-запроса модели, включая инкрементальный фильтр. В приведённом выше запуске было записано 378 строк: 377 заказов последнего дня, загруженных ранее, плюс один новый. - Создаётся таблица
orders__dbt_tmpс той же структурой, что иorders, и в неё копируются все строкиorders, у которыхorder_idотсутствует вorders__dbt_new_data. - Все строки
orders__dbt_new_dataвставляются вorders__dbt_tmp. Именно шаги 2 и 3 обеспечивают замену строк последнего дня вместо их дублирования. - Таблица
orders__dbt_new_dataудаляется. orders__dbt_tmpменяется местами сordersс помощью атомарного оператораEXCHANGE TABLES(через промежуточное переименование вorders__dbt_backup), так что теперьordersсодержит новую версию.- Старая версия удаляется.
Append strategy
Стратегияappend вставляет строки, отобранные моделью, напрямую в целевую таблицу. Временные таблицы не создаются, ничего не копируется — это самый дешёвый из возможных инкрементальных запусков. Плата за это — отсутствие дедупликации: если инкрементальный фильтр отберёт строку, которая уже есть в таблице, она попадёт туда во второй раз. Используйте эту стратегию для неизменяемых данных событийного характера и следите за тем, чтобы фильтр отбирал только действительно новые строки.
С усечённым до дня ordered_at это означает замену условия фильтра на >. Измените модель:
orders, — это один INSERT INTO jaffle_shop.orders ... SELECT ... с SQL модели и инкрементальным фильтром, и он записал одну строку.
Стратегия delete и insert
Исторически ClickHouse поддерживал обновления и удаления лишь ограниченно — в виде асинхронных мутаций. Они могут создавать крайне высокую нагрузку на ввод-вывод, поэтому их, как правило, следует избегать. В ClickHouse 22.8 появились легковесные удаления, а в ClickHouse 25.7 — легковесные обновления. Благодаря им результат отдельного оператора удаления или обновления виден пользователю сразу, хотя физически изменения применяются асинхронно. Стратегияdelete+insert основана на легковесных удалениях и настраивается через параметр incremental_strategy:
- Создаётся временная таблица (
orders__dbt_new_data_<run_id>), и в неё вставляются строки, выбранные моделью. - В таблице
ordersвыполняетсяDELETEдля каждогоorder_id, присутствующего во временной таблице. - Строки из временной таблицы вставляются в
orders. - Временная таблица удаляется.
Стратегия insert overwrite (экспериментальная)
Стратегияinsert_overwrite заменяет партиции целиком, поэтому ей требуется конфигурация partition_by — например, помесячная, как у orders. Она выполняет следующие шаги:
- Создаёт staging-таблицу (
orders__dbt_new_data_<run_id>) с той же структурой, что иorders. - Вставляет в staging-таблицу только те строки, которые выбраны моделью.
- Получает из
system.partsсписок партиций, присутствующих в staging-таблице. - Заменяет ровно эти партиции в
ordersкомандойALTER TABLE ... REPLACE PARTITION ... FROMиз staging-таблицы. - Удаляет staging-таблицу.
- Он быстрее стратегии по умолчанию, поскольку не копирует таблицу целиком.
- Он безопаснее остальных стратегий, поскольку не изменяет исходную таблицу, пока операция INSERT не завершится успешно: при промежуточном сбое исходная таблица остаётся неизменной.
- Он реализует принцип «неизменяемости партиций» — лучшую практику инженерии данных, которая упрощает инкрементальную и параллельную обработку данных, откаты и т. д.
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.
Создайте первый снимок:
generate_schema_name этого проекта помещает каждое отношение в целевую схему для непродакшн-целей, поэтому config schema у снимка вступит в силу только с целью prod. Она содержит по одной строке на каждого клиента, а также служебные столбцы dbt dbt_valid_from и dbt_valid_to; последний равен NULL для текущей версии строки:
orders и customers отразился новый заказ, а затем создайте второй снимок:
dbt_valid_to, а новая версия, где клиент уже имеет статус returning и два заказа, остаётся открытой. Данные по Danny не изменились, поэтому его строка осталась прежней:
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 суммирует числовые столбцы строк с одинаковым ключом сортировки — именно это и требуется для агрегата по дням и магазинам.
_mv, который ссылается на целевую таблицу через клаузу TO. По умолчанию (catchup=True) в целевую таблицу также была выполнена дозагрузка существующих заказов:
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, несколько представлений, наполняющих одну и ту же целевую таблицу, и определение целевой таблицы как отдельной модели.