В этом разделе приведены руководства по настройке dbt и адаптера ClickHouse, а также пример использования dbt с ClickHouse на общедоступном наборе данных IMDB. Пример включает следующие шаги:
- Создание проекта dbt и настройка адаптера ClickHouse.
- Определение модели.
- Обновление модели.
- Создание инкрементальной модели.
- Создание модели-снимка.
- Использование materialized views.
Эти руководства предназначены для использования вместе с остальной документацией, разделом возможностей и конфигураций и справочником по материализациям.
Настройка
Следуйте инструкциям в разделе Настройка dbt и адаптера ClickHouse, чтобы подготовить окружение.
Важно: приведённые ниже инструкции протестированы с Python 3.9.
Подготовьте ClickHouse
dbt особенно хорошо подходит для моделирования сильно связанных реляционных данных. В качестве примера мы используем небольшой набор данных IMDB со следующей реляционной схемой. Этот набор данных взят из репозитория реляционных наборов данных. По сравнению с типичными схемами, используемыми в dbt, он весьма прост, но при этом представляет собой удобный небольшой образец:

Как показано ниже, мы используем подмножество этих таблиц.
Создайте следующие таблицы:
CREATE DATABASE imdb;
CREATE TABLE imdb.actors
(
id UInt32,
first_name String,
last_name String,
gender FixedString(1)
) ENGINE = MergeTree ORDER BY (id, first_name, last_name, gender);
CREATE TABLE imdb.directors
(
id UInt32,
first_name String,
last_name String
) ENGINE = MergeTree ORDER BY (id, first_name, last_name);
CREATE TABLE imdb.genres
(
movie_id UInt32,
genre String
) ENGINE = MergeTree ORDER BY (movie_id, genre);
CREATE TABLE imdb.movie_directors
(
director_id UInt32,
movie_id UInt64
) ENGINE = MergeTree ORDER BY (director_id, movie_id);
CREATE TABLE imdb.movies
(
id UInt32,
name String,
year UInt32,
rank Float32 DEFAULT 0
) ENGINE = MergeTree ORDER BY (id, name, year);
CREATE TABLE imdb.roles
(
actor_id UInt32,
movie_id UInt32,
role String,
created_at DateTime DEFAULT now()
) ENGINE = MergeTree ORDER BY (actor_id, movie_id);Мы используем функцию s3, чтобы читать исходные данные из общедоступных конечных точек и выполнять вставку данных. Выполните следующие команды, чтобы заполнить таблицы:
INSERT INTO imdb.actors
SELECT *
FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/imdb/imdb_ijs_actors.tsv.gz', NOSIGN,
'TSVWithNames');
INSERT INTO imdb.directors
SELECT *
FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/imdb/imdb_ijs_directors.tsv.gz', NOSIGN,
'TSVWithNames');
INSERT INTO imdb.genres
SELECT *
FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/imdb/imdb_ijs_movies_genres.tsv.gz', NOSIGN,
'TSVWithNames');
INSERT INTO imdb.movie_directors
SELECT *
FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/imdb/imdb_ijs_movies_directors.tsv.gz', NOSIGN,
'TSVWithNames');
INSERT INTO imdb.movies
SELECT *
FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/imdb/imdb_ijs_movies.tsv.gz', NOSIGN,
'TSVWithNames');
INSERT INTO imdb.roles(actor_id, movie_id, role)
SELECT actor_id, movie_id, role
FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/imdb/imdb_ijs_roles.tsv.gz', NOSIGN,
'TSVWithNames');Время выполнения этих действий может различаться в зависимости от пропускной способности вашего соединения, но каждое из них должно занимать всего несколько секунд. Выполните следующий запрос, чтобы получить сводку по каждому актёру, отсортированную по количеству появлений в фильмах, и убедиться, что данные были успешно загружены:
SELECT id,
any(actor_name) AS name,
uniqExact(movie_id) AS num_movies,
avg(rank) AS avg_rank,
uniqExact(genre) AS unique_genres,
uniqExact(director_name) AS uniq_directors,
max(created_at) AS updated_at
FROM (
SELECT imdb.actors.id AS id,
concat(imdb.actors.first_name, ' ', imdb.actors.last_name) AS actor_name,
imdb.movies.id AS movie_id,
imdb.movies.rank AS rank,
genre,
concat(imdb.directors.first_name, ' ', imdb.directors.last_name) AS director_name,
created_at
FROM imdb.actors
JOIN imdb.roles ON imdb.roles.actor_id = imdb.actors.id
LEFT OUTER JOIN imdb.movies ON imdb.movies.id = imdb.roles.movie_id
LEFT OUTER JOIN imdb.genres ON imdb.genres.movie_id = imdb.movies.id
LEFT OUTER JOIN imdb.movie_directors ON imdb.movie_directors.movie_id = imdb.movies.id
LEFT OUTER JOIN imdb.directors ON imdb.directors.id = imdb.movie_directors.director_id
)
GROUP BY id
ORDER BY num_movies DESC
LIMIT 5;Ответ должен выглядеть следующим образом:
+------+------------+----------+------------------+-------------+--------------+-------------------+
|id |name |num_movies|avg_rank |unique_genres|uniq_directors|updated_at |
+------+------------+----------+------------------+-------------+--------------+-------------------+
|45332 |Mel Blanc |832 |6.175853582979779 |18 |84 |2022-04-26 14:01:45|
|621468|Bess Flowers|659 |5.57727638854796 |19 |293 |2022-04-26 14:01:46|
|372839|Lee Phelps |527 |5.032976449684617 |18 |261 |2022-04-26 14:01:46|
|283127|Tom London |525 |2.8721716524875673|17 |203 |2022-04-26 14:01:46|
|356804|Bud Osborne |515 |2.0389507108727773|15 |149 |2022-04-26 14:01:46|
+------+------------+----------+------------------+-------------+--------------+-------------------+В последующих руководствах мы преобразуем этот запрос в модель и материализуем её в ClickHouse как dbt-представление и таблицу.
Подключение к ClickHouse
-
Создайте проект dbt. В этом случае мы назовём его по имени нашего источника
imdb. Когда появится запрос, выберитеclickhouseв качестве источника базы данных.clickhouse-user@clickhouse:~$ dbt init imdb 16:52:40 Running with dbt=1.1.0 Which database would you like to use? [1] clickhouse (Don't see the one you want? https://docs.getdbt.com/docs/available-adapters) Enter a number: 1 16:53:21 No sample profile found for clickhouse. 16:53:21 Your new dbt project "imdb" was created! For more information on how to configure the profiles.yml file, please consult the dbt documentation here: https://docs.getdbt.com/docs/configure-your-profile -
Перейдите в каталог проекта с помощью
cd:cd imdb -
На этом этапе вам понадобится любой текстовый редактор. В примерах ниже мы используем популярный VS Code. Открыв каталог IMDB, вы должны увидеть набор файлов yml и sql:

-
Обновите файл
dbt_project.yml, чтобы указать нашу первую модель —actor_summary, и задайте профильclickhouse_imdb.

-
Далее нужно указать для dbt сведения о подключении к вашему экземпляру ClickHouse. Добавьте следующее в
~/.dbt/profiles.yml.clickhouse_imdb: target: dev outputs: dev: type: clickhouse schema: imdb_dbt host: localhost port: 8123 user: default password: '' secure: FalseОбратите внимание: нужно изменить имя пользователя и пароль. Дополнительные доступные настройки описаны здесь.
-
Находясь в каталоге IMDB, выполните команду
dbt debug, чтобы проверить, может ли dbt подключиться к ClickHouse.clickhouse-user@clickhouse:~/imdb$ dbt debug 17:33:53 Running with dbt=1.1.0 dbt version: 1.1.0 python version: 3.10.1 python path: /home/dale/.pyenv/versions/3.10.1/bin/python3.10 os info: Linux-5.13.0-10039-tuxedo-x86_64-with-glibc2.31 Using profiles.yml file at /home/dale/.dbt/profiles.yml Using dbt_project.yml file at /opt/dbt/imdb/dbt_project.yml Configuration: profiles.yml file [OK found and valid] dbt_project.yml file [OK found and valid] Required dependencies: - git [OK found] Connection: host: localhost port: 8123 user: default schema: imdb_dbt secure: False verify: False Connection test: [OK connection ok] All checks passed!Убедитесь, что в выводе есть строка
Connection test: [OK connection ok], которая указывает на успешное подключение.
Создание простой материализации представления
При использовании материализации представления модель при каждом запуске заново создаётся как представление с помощью оператора CREATE VIEW AS в ClickHouse. Это не требует дополнительного хранения данных, но запросы к такому представлению будут выполняться медленнее, чем при материализации в таблицы.
-
В папке
imdbудалите каталогmodels/example:clickhouse-user@clickhouse:~/imdb$ rm -rf models/example -
Создайте новый файл в каталоге
actorsвнутри папкиmodels. Здесь мы создаем файлы, каждый из которых соответствует отдельной модели actor:clickhouse-user@clickhouse:~/imdb$ mkdir models/actors -
Создайте файлы
schema.ymlиactor_summary.sqlв папкеmodels/actors.clickhouse-user@clickhouse:~/imdb$ touch models/actors/actor_summary.sql clickhouse-user@clickhouse:~/imdb$ touch models/actors/schema.ymlФайл
schema.ymlопределяет наши таблицы. После этого их можно будет использовать в макросах. Отредактируйтеmodels/actors/schema.yml, чтобы он содержал следующее содержимое:version: 2 sources: - name: imdb tables: - name: directors - name: actors - name: roles - name: movies - name: genres - name: movie_directorsactors_summary.sqlопределяет нашу фактическую модель. Обратите внимание, что в функцииconfigмы также указываем, что модель должна быть материализована как представление в ClickHouse. На наши таблицы есть ссылки из файлаschema.ymlчерез функциюsource, напримерsource('imdb', 'movies')ссылается на таблицуmoviesв базе данныхimdb. Отредактируйтеmodels/actors/actors_summary.sql, чтобы он содержал следующее:{{ config(materialized='view') }} with actor_summary as ( SELECT id, any(actor_name) as name, uniqExact(movie_id) as num_movies, avg(rank) as avg_rank, uniqExact(genre) as genres, uniqExact(director_name) as directors, max(created_at) as updated_at FROM ( SELECT {{ source('imdb', 'actors') }}.id as id, concat({{ source('imdb', 'actors') }}.first_name, ' ', {{ source('imdb', 'actors') }}.last_name) as actor_name, {{ source('imdb', 'movies') }}.id as movie_id, {{ source('imdb', 'movies') }}.rank as rank, genre, concat({{ source('imdb', 'directors') }}.first_name, ' ', {{ source('imdb', 'directors') }}.last_name) as director_name, created_at FROM {{ source('imdb', 'actors') }} JOIN {{ source('imdb', 'roles') }} ON {{ source('imdb', 'roles') }}.actor_id = {{ source('imdb', 'actors') }}.id LEFT OUTER JOIN {{ source('imdb', 'movies') }} ON {{ source('imdb', 'movies') }}.id = {{ source('imdb', 'roles') }}.movie_id LEFT OUTER JOIN {{ source('imdb', 'genres') }} ON {{ source('imdb', 'genres') }}.movie_id = {{ source('imdb', 'movies') }}.id LEFT OUTER JOIN {{ source('imdb', 'movie_directors') }} ON {{ source('imdb', 'movie_directors') }}.movie_id = {{ source('imdb', 'movies') }}.id LEFT OUTER JOIN {{ source('imdb', 'directors') }} ON {{ source('imdb', 'directors') }}.id = {{ source('imdb', 'movie_directors') }}.director_id ) GROUP BY id ) select * from actor_summaryОбратите внимание, что мы включаем столбец
updated_atв итоговое actor_summary. Позже он понадобится для инкрементальных материализаций. -
В каталоге
imdbвыполните командуdbt run.clickhouse-user@clickhouse:~/imdb$ dbt run 15:05:35 Running with dbt=1.1.0 15:05:35 Found 1 model, 0 tests, 1 snapshot, 0 analyses, 181 macros, 0 operations, 0 seed files, 6 sources, 0 exposures, 0 metrics 15:05:35 15:05:36 Concurrency: 1 threads (target='dev') 15:05:36 15:05:36 1 of 1 START view model imdb_dbt.actor_summary.................................. [RUN] 15:05:37 1 of 1 OK created view model imdb_dbt.actor_summary............................. [OK in 1.00s] 15:05:37 15:05:37 Finished running 1 view model in 1.97s. 15:05:37 15:05:37 Completed successfully 15:05:37 15:05:37 Done. PASS=1 WARN=0 ERROR=0 SKIP=0 TOTAL=1 -
dbt представит модель как представление в ClickHouse, как и было запрошено. Теперь мы можем выполнять запросы к этому представлению напрямую. Это представление будет создано в базе данных
imdb_dbt— это определяется параметромschemaв файле~/.dbt/profiles.ymlв профилеclickhouse_imdb.SHOW DATABASES;+------------------+ |name | +------------------+ |INFORMATION_SCHEMA| |default | |imdb | |imdb_dbt | <---создано dbt! |information_schema| |system | +------------------+Выполнив запрос к этому представлению, мы можем получить те же результаты, что и в предыдущем запросе, но с более простым синтаксисом:
SELECT * FROM imdb_dbt.actor_summary ORDER BY num_movies DESC LIMIT 5;+------+------------+----------+------------------+------+---------+-------------------+ |id |name |num_movies|avg_rank |genres|directors|updated_at | +------+------------+----------+------------------+------+---------+-------------------+ |45332 |Mel Blanc |832 |6.175853582979779 |18 |84 |2022-04-26 15:26:55| |621468|Bess Flowers|659 |5.57727638854796 |19 |293 |2022-04-26 15:26:57| |372839|Lee Phelps |527 |5.032976449684617 |18 |261 |2022-04-26 15:26:56| |283127|Tom London |525 |2.8721716524875673|17 |203 |2022-04-26 15:26:56| |356804|Bud Osborne |515 |2.0389507108727773|15 |149 |2022-04-26 15:26:56| +------+------------+----------+------------------+------+---------+-------------------+
Создание материализации в таблицу
В предыдущем примере наша модель была материализована как представление. Хотя для некоторых запросов этого может быть достаточно, более сложные SELECT-запросы или часто выполняемые запросы лучше материализовать в таблицу. Такая материализация полезна для моделей, к которым обращаются BI-инструменты, чтобы обеспечить пользователям более высокую скорость работы. По сути, результаты запроса сохраняются в новой таблице с соответствующими накладными расходами на хранение — фактически выполняется INSERT TO SELECT. Обратите внимание, что эта таблица будет пересоздаваться каждый раз, то есть она не является инкрементальной. Поэтому большие результирующие наборы могут приводить к длительному времени выполнения — см. Ограничения dbt.
-
Измените файл
actors_summary.sql, чтобы параметрmaterializedбыл установлен вtable. Обратите внимание, как заданORDER BY, а также на то, что мы используем движок таблицыMergeTree:{{ config(order_by='(updated_at, id, name)', engine='MergeTree()', materialized='table') }} -
Из каталога
imdbвыполните командуdbt run. Выполнение может занять немного больше времени — около 10 с на большинстве машин.clickhouse-user@clickhouse:~/imdb$ dbt run 15:13:27 Running with dbt=1.1.0 15:13:27 Found 1 model, 0 tests, 1 snapshot, 0 analyses, 181 macros, 0 operations, 0 seed files, 6 sources, 0 exposures, 0 metrics 15:13:27 15:13:28 Concurrency: 1 threads (target='dev') 15:13:28 15:13:28 1 of 1 START table model imdb_dbt.actor_summary................................. [RUN] 15:13:37 1 of 1 OK created table model imdb_dbt.actor_summary............................ [OK in 9.22s] 15:13:37 15:13:37 Finished running 1 table model in 10.20s. 15:13:37 15:13:37 Completed successfully 15:13:37 15:13:37 Done. PASS=1 WARN=0 ERROR=0 SKIP=0 TOTAL=1 -
Подтвердите создание таблицы
imdb_dbt.actor_summary:SHOW CREATE TABLE imdb_dbt.actor_summary;Вы должны увидеть таблицу с соответствующими типами данных:
+---------------------------------------- |statement +---------------------------------------- |CREATE TABLE imdb_dbt.actor_summary |( |`id` UInt32, |`first_name` String, |`last_name` String, |`num_movies` UInt64, |`updated_at` DateTime |) |ENGINE = MergeTree |ORDER BY (id, first_name, last_name) +---------------------------------------- -
Убедитесь, что результаты из этой таблицы совпадают с предыдущими результатами. Обратите внимание на заметное улучшение времени отклика теперь, когда модель материализована как таблица:
SELECT * FROM imdb_dbt.actor_summary ORDER BY num_movies DESC LIMIT 5;+------+------------+----------+------------------+------+---------+-------------------+ |id |name |num_movies|avg_rank |genres|directors|updated_at | +------+------------+----------+------------------+------+---------+-------------------+ |45332 |Mel Blanc |832 |6.175853582979779 |18 |84 |2022-04-26 15:26:55| |621468|Bess Flowers|659 |5.57727638854796 |19 |293 |2022-04-26 15:26:57| |372839|Lee Phelps |527 |5.032976449684617 |18 |261 |2022-04-26 15:26:56| |283127|Tom London |525 |2.8721716524875673|17 |203 |2022-04-26 15:26:56| |356804|Bud Osborne |515 |2.0389507108727773|15 |149 |2022-04-26 15:26:56| +------+------------+----------+------------------+------+---------+-------------------+При желании можете выполнить и другие запросы к этой модели. Например, у каких актёров самые высоко оценённые фильмы среди тех, кто снялся более чем в 5 фильмах?
SELECT * FROM imdb_dbt.actor_summary WHERE num_movies > 5 ORDER BY avg_rank DESC LIMIT 10;
Создание инкрементальной материализации
В предыдущем примере была создана таблица для материализации модели. Эта таблица будет пересоздаваться при каждом запуске dbt. Для больших результирующих наборов или сложных преобразований это может быть непрактично и чрезвычайно затратно. Чтобы решить эту проблему и сократить время сборки, dbt предлагает инкрементальные материализации. Они позволяют dbt выполнять вставку или обновление записей в таблице с момента последнего запуска, что делает такой подход подходящим для данных событийного типа. Внутри создаётся временная таблица со всеми обновлёнными записями, после чего все неизменённые и обновлённые записи вставляются в новую целевую таблицу. В результате для больших результирующих наборов возникают ограничения, аналогичные ограничениям модели table.
Чтобы обойти эти ограничения для больших наборов данных, адаптер поддерживает режим 'inserts_only', при котором все обновления вставляются в целевую таблицу без создания временной таблицы (подробнее об этом ниже).
Чтобы проиллюстрировать этот пример, мы добавим актёра "Clicky McClickHouse", который появится в невероятных 910 фильмах, — это гарантирует, что он снялся в большем числе фильмов, чем даже Mel Blanc.
-
Сначала изменим нашу модель, задав для неё тип incremental. Это требует:
- unique_key - Чтобы адаптер мог однозначно идентифицировать строки, необходимо указать unique_key — в данном случае достаточно поля
idиз нашего запроса. Это гарантирует отсутствие дубликатов строк в нашей материализованной таблице. Подробнее об ограничениях уникальности см. здесь. - Incremental filter - Нам также нужно указать dbt, как определять, какие строки изменились при инкрементальном запуске. Для этого задаётся дельта-выражение. Обычно для данных событий используется временная метка, поэтому мы берём поле updated_at. Этот столбец, которому при вставке строк по умолчанию присваивается значение now(), позволяет выявлять новые роли. Кроме того, нужно учесть альтернативный сценарий, когда добавляются новые акторы. Используя переменную
{{this}}для обозначения существующей материализованной таблицы, получаем выражениеwhere id > (select max(id) from {{ this }}) or updated_at > (select max(updated_at) from {{this}}). Мы помещаем его внутрь условия{% if is_incremental() %}, чтобы оно применялось только при инкрементальных запусках, а не при первоначальном создании таблицы. Подробнее о фильтрации строк для инкрементальных моделей см. в этом разделе документации dbt.
Обновите файл
actor_summary.sqlследующим образом:{{ config(order_by='(updated_at, id, name)', engine='MergeTree()', materialized='incremental', unique_key='id') }} with actor_summary as ( SELECT id, any(actor_name) as name, uniqExact(movie_id) as num_movies, avg(rank) as avg_rank, uniqExact(genre) as genres, uniqExact(director_name) as directors, max(created_at) as updated_at FROM ( SELECT {{ source('imdb', 'actors') }}.id as id, concat({{ source('imdb', 'actors') }}.first_name, ' ', {{ source('imdb', 'actors') }}.last_name) as actor_name, {{ source('imdb', 'movies') }}.id as movie_id, {{ source('imdb', 'movies') }}.rank as rank, genre, concat({{ source('imdb', 'directors') }}.first_name, ' ', {{ source('imdb', 'directors') }}.last_name) as director_name, created_at FROM {{ source('imdb', 'actors') }} JOIN {{ source('imdb', 'roles') }} ON {{ source('imdb', 'roles') }}.actor_id = {{ source('imdb', 'actors') }}.id LEFT OUTER JOIN {{ source('imdb', 'movies') }} ON {{ source('imdb', 'movies') }}.id = {{ source('imdb', 'roles') }}.movie_id LEFT OUTER JOIN {{ source('imdb', 'genres') }} ON {{ source('imdb', 'genres') }}.movie_id = {{ source('imdb', 'movies') }}.id LEFT OUTER JOIN {{ source('imdb', 'movie_directors') }} ON {{ source('imdb', 'movie_directors') }}.movie_id = {{ source('imdb', 'movies') }}.id LEFT OUTER JOIN {{ source('imdb', 'directors') }} ON {{ source('imdb', 'directors') }}.id = {{ source('imdb', 'movie_directors') }}.director_id ) GROUP BY id ) select * from actor_summary {% if is_incremental() %} -- этот фильтр применяется только при инкрементальном запуске where id > (select max(id) from {{ this }}) or updated_at > (select max(updated_at) from {{this}}) {% endif %}Обратите внимание, что наша модель будет реагировать только на обновления и добавления в таблицах
rolesиactors. Чтобы она реагировала на все таблицы, рекомендуется разделить эту модель на несколько подмоделей, каждая из которых будет иметь собственные критерии инкрементальности. На эти модели, в свою очередь, можно ссылаться и связывать их между собой. Дополнительные сведения о перекрёстных ссылках между моделями см. здесь. - unique_key - Чтобы адаптер мог однозначно идентифицировать строки, необходимо указать unique_key — в данном случае достаточно поля
-
Выполните
dbt runи проверьте результаты в созданной таблице:clickhouse-user@clickhouse:~/imdb$ dbt run 15:33:34 Running with dbt=1.1.0 15:33:34 Found 1 model, 0 tests, 1 snapshot, 0 analyses, 181 macros, 0 operations, 0 seed files, 6 sources, 0 exposures, 0 metrics 15:33:34 15:33:35 Concurrency: 1 threads (target='dev') 15:33:35 15:33:35 1 of 1 START incremental model imdb_dbt.actor_summary........................... [RUN] 15:33:41 1 of 1 OK created incremental model imdb_dbt.actor_summary...................... [OK in 6.33s] 15:33:41 15:33:41 Finished running 1 incremental model in 7.30s. 15:33:41 15:33:41 Completed successfully 15:33:41 15:33:41 Done. PASS=1 WARN=0 ERROR=0 SKIP=0 TOTAL=1SELECT * FROM imdb_dbt.actor_summary ORDER BY num_movies DESC LIMIT 5;+------+------------+----------+------------------+------+---------+-------------------+ |id |name |num_movies|avg_rank |genres|directors|updated_at | +------+------------+----------+------------------+------+---------+-------------------+ |45332 |Mel Blanc |832 |6.175853582979779 |18 |84 |2022-04-26 15:26:55| |621468|Bess Flowers|659 |5.57727638854796 |19 |293 |2022-04-26 15:26:57| |372839|Lee Phelps |527 |5.032976449684617 |18 |261 |2022-04-26 15:26:56| |283127|Tom London |525 |2.8721716524875673|17 |203 |2022-04-26 15:26:56| |356804|Bud Osborne |515 |2.0389507108727773|15 |149 |2022-04-26 15:26:56| +------+------------+----------+------------------+------+---------+-------------------+ -
Теперь добавим в нашу модель данные, чтобы показать инкрементное обновление. Добавьте актёра "Clicky McClickHouse" в таблицу
actors:INSERT INTO imdb.actors VALUES (845466, 'Clicky', 'McClickHouse', 'M'); -
Пусть «Clicky» появится в 910 случайных фильмах:
INSERT INTO imdb.roles SELECT now() as created_at, 845466 as actor_id, id as movie_id, 'Himself' as role FROM imdb.movies LIMIT 910 OFFSET 10000; -
Подтвердите, что теперь именно он — актёр с наибольшим числом появлений, выполнив запрос напрямую к исходной таблице в обход любых моделей dbt:
SELECT id, any(actor_name) as name, uniqExact(movie_id) as num_movies, avg(rank) as avg_rank, uniqExact(genre) as unique_genres, uniqExact(director_name) as uniq_directors, max(created_at) as updated_at FROM ( SELECT imdb.actors.id as id, concat(imdb.actors.first_name, ' ', imdb.actors.last_name) as actor_name, imdb.movies.id as movie_id, imdb.movies.rank as rank, genre, concat(imdb.directors.first_name, ' ', imdb.directors.last_name) as director_name, created_at FROM imdb.actors JOIN imdb.roles ON imdb.roles.actor_id = imdb.actors.id LEFT OUTER JOIN imdb.movies ON imdb.movies.id = imdb.roles.movie_id LEFT OUTER JOIN imdb.genres ON imdb.genres.movie_id = imdb.movies.id LEFT OUTER JOIN imdb.movie_directors ON imdb.movie_directors.movie_id = imdb.movies.id LEFT OUTER JOIN imdb.directors ON imdb.directors.id = imdb.movie_directors.director_id ) GROUP BY id ORDER BY num_movies DESC LIMIT 2;+------+-------------------+----------+------------------+------+---------+-------------------+ |id |name |num_movies|avg_rank |genres|directors|updated_at | +------+-------------------+----------+------------------+------+---------+-------------------+ |845466|Clicky McClickHouse|910 |1.4687938697032283|21 |662 |2022-04-26 16:20:36| |45332 |Mel Blanc |909 |5.7884792542982515|19 |148 |2022-04-26 16:17:42| +------+-------------------+----------+------------------+------+---------+-------------------+ -
Выполните
dbt runи убедитесь, что наша модель обновилась и соответствует приведённым выше результатам:clickhouse-user@clickhouse:~/imdb$ dbt run 16:12:16 Running with dbt=1.1.0 16:12:16 Found 1 model, 0 tests, 1 snapshot, 0 analyses, 181 macros, 0 operations, 0 seed files, 6 sources, 0 exposures, 0 metrics 16:12:16 16:12:17 Concurrency: 1 threads (target='dev') 16:12:17 16:12:17 1 of 1 START incremental model imdb_dbt.actor_summary........................... [RUN] 16:12:24 1 of 1 OK created incremental model imdb_dbt.actor_summary...................... [OK in 6.82s] 16:12:24 16:12:24 Finished running 1 incremental model in 7.79s. 16:12:24 16:12:24 Completed successfully 16:12:24 16:12:24 Done. PASS=1 WARN=0 ERROR=0 SKIP=0 TOTAL=1SELECT * FROM imdb_dbt.actor_summary ORDER BY num_movies DESC LIMIT 2;+------+-------------------+----------+------------------+------+---------+-------------------+ |id |name |num_movies|avg_rank |genres|directors|updated_at | +------+-------------------+----------+------------------+------+---------+-------------------+ |845466|Clicky McClickHouse|910 |1.4687938697032283|21 |662 |2022-04-26 16:20:36| |45332 |Mel Blanc |909 |5.7884792542982515|19 |148 |2022-04-26 16:17:42| +------+-------------------+----------+------------------+------+---------+-------------------+
Внутреннее устройство
Мы можем определить, какие команды были выполнены для описанного выше инкрементального обновления, выполнив запрос к журналу запросов ClickHouse.
SELECT event_time, query FROM system.query_log WHERE type='QueryStart' AND query LIKE '%dbt%'
AND event_time > subtractMinutes(now(), 15) ORDER BY event_time LIMIT 100;Скорректируйте приведённый выше запрос под период выполнения. Анализ результатов оставляем пользователю, а здесь выделим общую стратегию, которую адаптер использует для инкрементальных обновлений:
- Адаптер создаёт временную таблицу
actor_sumary__dbt_tmp. В неё передаются изменившиеся строки. - Создаётся новая таблица
actor_summary_new,. Затем строки из старой таблицы переносятся в новую, при этом выполняется проверка, чтобы идентификаторы строк отсутствовали во временной таблице. Это позволяет корректно обрабатывать обновления и дубликаты. - Результаты из временной таблицы переносятся в новую таблицу
actor_summary: - Наконец, новая таблица атомарно обменивается со старой версией с помощью оператора
EXCHANGE TABLES. После этого старая и временная таблицы удаляются.
Это показано ниже:

Эта стратегия может вызывать трудности при работе с очень большими моделями. Подробнее см. в разделе Ограничения.
Стратегия Append (режим только вставки)
Чтобы обойти ограничения, связанные с большими наборами данных в инкрементальных моделях, адаптер использует параметр конфигурации dbt incremental_strategy. Ему можно задать значение append. В этом случае обновленные строки вставляются напрямую в целевую таблицу (то есть imdb_dbt.actor_summary), а временная таблица не создается.
Примечание: режим append-only требует, чтобы данные были неизменяемыми или чтобы дубликаты считались допустимыми. Если вам нужна инкрементальная модель таблицы с поддержкой изменяемых строк, не используйте этот режим!
Чтобы продемонстрировать этот режим, мы добавим еще одного нового актера и снова выполним dbt run с incremental_strategy='append'.
-
Настройте режим append-only в actor_summary.sql:
{{ config(order_by='(updated_at, id, name)', engine='MergeTree()', materialized='incremental', unique_key='id', incremental_strategy='append') }} -
Добавим еще одного известного актера — Danny DeBito
INSERT INTO imdb.actors VALUES (845467, 'Danny', 'DeBito', 'M'); -
Дадим Danny роли в 920 случайных фильмах.
INSERT INTO imdb.roles SELECT now() as created_at, 845467 as actor_id, id as movie_id, 'Himself' as role FROM imdb.movies LIMIT 920 OFFSET 10000; -
Выполните
dbt runи убедитесь, что Danny был добавлен в таблицуactor_summaryclickhouse-user@clickhouse:~/imdb$ dbt run 16:12:16 Running with dbt=1.1.0 16:12:16 Found 1 model, 0 tests, 1 snapshot, 0 analyses, 186 macros, 0 operations, 0 seed files, 6 sources, 0 exposures, 0 metrics 16:12:16 16:12:17 Concurrency: 1 threads (target='dev') 16:12:17 16:12:17 1 of 1 START incremental model imdb_dbt.actor_summary........................... [RUN] 16:12:24 1 of 1 OK created incremental model imdb_dbt.actor_summary...................... [OK in 0.17s] 16:12:24 16:12:24 Finished running 1 incremental model in 0.19s. 16:12:24 16:12:24 Completed successfully 16:12:24 16:12:24 Done. PASS=1 WARN=0 ERROR=0 SKIP=0 TOTAL=1SELECT * FROM imdb_dbt.actor_summary ORDER BY num_movies DESC LIMIT 3;+------+-------------------+----------+------------------+------+---------+-------------------+ |id |name |num_movies|avg_rank |genres|directors|updated_at | +------+-------------------+----------+------------------+------+---------+-------------------+ |845467|Danny DeBito |920 |1.4768987303293204|21 |670 |2022-04-26 16:22:06| |845466|Clicky McClickHouse|910 |1.4687938697032283|21 |662 |2022-04-26 16:20:36| |45332 |Mel Blanc |909 |5.7884792542982515|19 |148 |2022-04-26 16:17:42| +------+-------------------+----------+------------------+------+---------+-------------------+
Обратите внимание, насколько быстрее выполнился этот инкрементальный запуск по сравнению со вставкой для "Clicky".
Повторная проверка таблицы query_log показывает различия между двумя инкрементальными запусками:
INSERT INTO imdb_dbt.actor_summary ("id", "name", "num_movies", "avg_rank", "genres", "directors", "updated_at")
WITH actor_summary AS (
SELECT id,
any(actor_name) AS name,
uniqExact(movie_id) AS num_movies,
avg(rank) AS avg_rank,
uniqExact(genre) AS genres,
uniqExact(director_name) AS directors,
max(created_at) AS updated_at
FROM (
SELECT imdb.actors.id AS id,
concat(imdb.actors.first_name, ' ', imdb.actors.last_name) AS actor_name,
imdb.movies.id AS movie_id,
imdb.movies.rank AS rank,
genre,
concat(imdb.directors.first_name, ' ', imdb.directors.last_name) AS director_name,
created_at
FROM imdb.actors
JOIN imdb.roles ON imdb.roles.actor_id = imdb.actors.id
LEFT OUTER JOIN imdb.movies ON imdb.movies.id = imdb.roles.movie_id
LEFT OUTER JOIN imdb.genres ON imdb.genres.movie_id = imdb.movies.id
LEFT OUTER JOIN imdb.movie_directors ON imdb.movie_directors.movie_id = imdb.movies.id
LEFT OUTER JOIN imdb.directors ON imdb.directors.id = imdb.movie_directors.director_id
)
GROUP BY id
)
SELECT *
FROM actor_summary
-- этот фильтр применяется только при инкрементальном запуске
WHERE id > (SELECT max(id) FROM imdb_dbt.actor_summary) OR updated_at > (SELECT max(updated_at) FROM imdb_dbt.actor_summary)В этом запуске в таблицу imdb_dbt.actor_summary напрямую добавляются только новые строки, без создания таблицы.
Режим удаления и вставки (экспериментальный)
Изначально в ClickHouse была лишь ограниченная поддержка обновлений и удалений в виде асинхронных Мутаций. Они могут быть чрезвычайно затратными по I/O, поэтому их обычно следует избегать.
В ClickHouse 22.8 появились легковесные удаления, а в ClickHouse 25.7 — легковесные обновления. С появлением этих возможностей изменения, вносимые отдельными запросами на обновление, даже при асинхронной материализации становятся мгновенно видимыми для пользователя.
Этот режим можно настроить для модели с помощью параметра incremental_strategy, например:
{{ config(order_by='(updated_at, id, name)', engine='MergeTree()', materialized='incremental', unique_key='id', incremental_strategy='delete+insert') }}Эта стратегия работает напрямую с таблицей целевой модели, поэтому, если во время выполнения возникнет проблема, данные в инкрементальной модели, скорее всего, окажутся в некорректном состоянии — атомарного обновления здесь нет.
Вкратце этот подход выглядит так:
- Адаптер создаёт временную таблицу
actor_sumary__dbt_tmp. Изменённые строки направляются в эту таблицу. - Для текущей таблицы
actor_summaryвыполняетсяDELETE. Строки удаляются по id изactor_sumary__dbt_tmp - Строки из
actor_sumary__dbt_tmpвставляются вactor_summaryс помощьюINSERT INTO actor_summary SELECT * FROM actor_sumary__dbt_tmp.
Ниже показан этот процесс:

Режим insert_overwrite (экспериментальный)
Включает следующие шаги:
- Создать staging-таблицу (временную таблицу) с той же структурой, что и отношение инкрементальной модели:
CREATE TABLE {staging} AS {target}. - Выполнить вставку в staging-таблицу только новых записей (полученных с помощью SELECT).
- Заменить в целевой таблице только новые партиции (присутствующие в staging-таблице).
У этого подхода есть следующие преимущества:
- Он быстрее стратегии по умолчанию, поскольку не копирует всю таблицу.
- Он безопаснее других стратегий, поскольку не изменяет исходную таблицу, пока операция INSERT не завершится успешно: в случае сбоя на промежуточном этапе исходная таблица не изменяется.
- Он реализует рекомендуемую в дата-инжиниринге практику «неизменяемости партиций», что упрощает инкрементальную и параллельную обработку данных, откаты и т. д.

Создание снимка
Снимки dbt позволяют сохранять историю изменений изменяемой модели с течением времени. Это, в свою очередь, позволяет выполнять запросы к моделям на определённый момент времени, чтобы аналитики могли «вернуться назад во времени» и посмотреть на предыдущее состояние модели. Это достигается с помощью медленно изменяющихся измерений типа 2, где столбцы с датами начала и окончания фиксируют, в какой период строка была актуальной. Эта функциональность поддерживается адаптером ClickHouse и показана ниже.
В этом примере предполагается, что вы уже выполнили шаг Создание инкрементной табличной модели. Убедитесь, что в вашем actor_summary.sql не задано inserts_only=True. Файл models/actor_summary.sql должен выглядеть так:
{{ config(order_by='(updated_at, id, name)', engine='MergeTree()', materialized='incremental', unique_key='id') }}
with actor_summary as (
SELECT id,
any(actor_name) as name,
uniqExact(movie_id) as num_movies,
avg(rank) as avg_rank,
uniqExact(genre) as genres,
uniqExact(director_name) as directors,
max(created_at) as updated_at
FROM (
SELECT {{ source('imdb', 'actors') }}.id as id,
concat({{ source('imdb', 'actors') }}.first_name, ' ', {{ source('imdb', 'actors') }}.last_name) as actor_name,
{{ source('imdb', 'movies') }}.id as movie_id,
{{ source('imdb', 'movies') }}.rank as rank,
genre,
concat({{ source('imdb', 'directors') }}.first_name, ' ', {{ source('imdb', 'directors') }}.last_name) as director_name,
created_at
FROM {{ source('imdb', 'actors') }}
JOIN {{ source('imdb', 'roles') }} ON {{ source('imdb', 'roles') }}.actor_id = {{ source('imdb', 'actors') }}.id
LEFT OUTER JOIN {{ source('imdb', 'movies') }} ON {{ source('imdb', 'movies') }}.id = {{ source('imdb', 'roles') }}.movie_id
LEFT OUTER JOIN {{ source('imdb', 'genres') }} ON {{ source('imdb', 'genres') }}.movie_id = {{ source('imdb', 'movies') }}.id
LEFT OUTER JOIN {{ source('imdb', 'movie_directors') }} ON {{ source('imdb', 'movie_directors') }}.movie_id = {{ source('imdb', 'movies') }}.id
LEFT OUTER JOIN {{ source('imdb', 'directors') }} ON {{ source('imdb', 'directors') }}.id = {{ source('imdb', 'movie_directors') }}.director_id
)
GROUP BY id
)
select *
from actor_summary
{% if is_incremental() %}
-- этот фильтр будет применяться только при инкрементальном запуске
where id > (select max(id) from {{ this }}) or updated_at > (select max(updated_at) from {{this}})
{% endif %}-
Создайте файл
actor_summaryв каталоге snapshots.touch snapshots/actor_summary.sql -
Обновите содержимое файла actor_summary.sql следующим образом:
{% snapshot actor_summary_snapshot %} {{ config( target_schema='snapshots', unique_key='id', strategy='timestamp', updated_at='updated_at', ) }} select * from {{ref('actor_summary')}} {% endsnapshot %}
Несколько замечаний по этому содержимому:
- Запрос
selectопределяет результаты, снимки которых вы хотите сохранять с течением времени. Функция ref используется, чтобы сослаться на ранее созданную модель actor_summary. - Нам нужен столбец с временной меткой, чтобы отмечать изменения в записях. Здесь можно использовать наш столбец updated_at (см. Создание инкрементной модели таблицы). Параметр strategy указывает, что для отслеживания обновлений мы используем временную метку, а параметр updated_at задает, какой столбец использовать. Если этого столбца нет в вашей модели, можно вместо этого использовать стратегию check. Это существенно менее эффективно и требует указать список столбцов для сравнения. dbt сравнивает текущие и исторические значения этих столбцов, фиксируя любые изменения (или ничего не делает, если значения совпадают).
-
Выполните команду
dbt snapshot.clickhouse-user@clickhouse:~/imdb$ dbt snapshot 13:26:23 Running with dbt=1.1.0 13:26:23 Found 1 model, 0 tests, 1 snapshot, 0 analyses, 181 macros, 0 operations, 0 seed files, 3 sources, 0 exposures, 0 metrics 13:26:23 13:26:25 Concurrency: 1 threads (target='dev') 13:26:25 13:26:25 1 of 1 START snapshot snapshots.actor_summary_snapshot...................... [RUN] 13:26:25 1 of 1 OK snapshotted snapshots.actor_summary_snapshot...................... [OK in 0.79s] 13:26:25 13:26:25 Finished running 1 snapshot in 2.11s. 13:26:25 13:26:25 Completed successfully 13:26:25 13:26:25 Done. PASS=1 WARN=0 ERROR=0 SKIP=0 TOTAL=1
Обратите внимание, что в базе данных snapshots была создана таблица actor_summary_snapshot (это задаётся параметром target_schema).
-
Выбрав эти данные, вы увидите, что dbt добавил столбцы dbt_valid_from и dbt_valid_to. У последнего значения равны null. При последующих запусках это обновится.
SELECT id, name, num_movies, dbt_valid_from, dbt_valid_to FROM snapshots.actor_summary_snapshot ORDER BY num_movies DESC LIMIT 5;+------+----------+------------+----------+-------------------+------------+ |id |first_name|last_name |num_movies|dbt_valid_from |dbt_valid_to| +------+----------+------------+----------+-------------------+------------+ |845467|Danny |DeBito |920 |2022-05-25 19:33:32|NULL | |845466|Clicky |McClickHouse|910 |2022-05-25 19:32:34|NULL | |45332 |Mel |Blanc |909 |2022-05-25 19:31:47|NULL | |621468|Bess |Flowers |672 |2022-05-25 19:31:47|NULL | |283127|Tom |London |549 |2022-05-25 19:31:47|NULL | +------+----------+------------+----------+-------------------+------------+ -
Пусть наш любимый актёр Clicky McClickHouse снимется ещё в 10 фильмах.
INSERT INTO imdb.roles SELECT now() as created_at, 845466 as actor_id, rand(number) % 412320 as movie_id, 'Himself' as role FROM system.numbers LIMIT 10; -
Снова выполните команду dbt run из каталога
imdb. Это обновит инкрементную модель. Когда процесс завершится, выполните dbt snapshot, чтобы зафиксировать изменения.clickhouse-user@clickhouse:~/imdb$ dbt run 13:46:14 Running with dbt=1.1.0 13:46:14 Found 1 model, 0 tests, 1 snapshot, 0 analyses, 181 macros, 0 operations, 0 seed files, 3 sources, 0 exposures, 0 metrics 13:46:14 13:46:15 Concurrency: 1 threads (target='dev') 13:46:15 13:46:15 1 of 1 START incremental model imdb_dbt.actor_summary....................... [RUN] 13:46:18 1 of 1 OK created incremental model imdb_dbt.actor_summary.................. [OK in 2.76s] 13:46:18 13:46:18 Finished running 1 incremental model in 3.73s. 13:46:18 13:46:18 Completed successfully 13:46:18 13:46:18 Done. PASS=1 WARN=0 ERROR=0 SKIP=0 TOTAL=1 clickhouse-user@clickhouse:~/imdb$ dbt snapshot 13:46:26 Running with dbt=1.1.0 13:46:26 Found 1 model, 0 tests, 1 snapshot, 0 analyses, 181 macros, 0 operations, 0 seed files, 3 sources, 0 exposures, 0 metrics 13:46:26 13:46:27 Concurrency: 1 threads (target='dev') 13:46:27 13:46:27 1 of 1 START snapshot snapshots.actor_summary_snapshot...................... [RUN] 13:46:31 1 of 1 OK snapshotted snapshots.actor_summary_snapshot...................... [OK in 4.05s] 13:46:31 13:46:31 Finished running 1 snapshot in 5.02s. 13:46:31 13:46:31 Completed successfully 13:46:31 13:46:31 Done. PASS=1 WARN=0 ERROR=0 SKIP=0 TOTAL=1 -
Если теперь выполнить запрос к нашему снимку, обратите внимание: у нас есть 2 строки для Clicky McClickHouse. В нашей предыдущей записи теперь заполнено значение dbt_valid_to. Новое значение записано с тем же значением в столбце dbt_valid_from, а значение dbt_valid_to равно null. Если бы у нас были новые строки, они также были бы добавлены в снимок.
SELECT id, name, num_movies, dbt_valid_from, dbt_valid_to FROM snapshots.actor_summary_snapshot ORDER BY num_movies DESC LIMIT 5;+------+----------+------------+----------+-------------------+-------------------+ |id |first_name|last_name |num_movies|dbt_valid_from |dbt_valid_to | +------+----------+------------+----------+-------------------+-------------------+ |845467|Danny |DeBito |920 |2022-05-25 19:33:32|NULL | |845466|Clicky |McClickHouse|920 |2022-05-25 19:34:37|NULL | |845466|Clicky |McClickHouse|910 |2022-05-25 19:32:34|2022-05-25 19:34:37| |45332 |Mel |Blanc |909 |2022-05-25 19:31:47|NULL | |621468|Bess |Flowers |672 |2022-05-25 19:31:47|NULL | +------+----------+------------+----------+-------------------+-------------------+
Подробные сведения о снимках dbt см. здесь.
Использование seed-файлов
dbt предоставляет возможность загружать данные из CSV-файлов. Эта возможность не подходит для загрузки больших выгрузок из базы данных и в большей степени рассчитана на небольшие файлы, обычно используемые для кодовых таблиц и словарей, например для сопоставления кодов стран с названиями стран. В качестве простого примера мы сгенерируем, а затем загрузим список кодов жанров с помощью механизма seed.
-
Мы генерируем список кодов жанров из имеющегося набора данных. В каталоге dbt используйте
clickhouse-client, чтобы создать файлseeds/genre_codes.csv:clickhouse-user@clickhouse:~/imdb$ clickhouse-client --password <password> --query "SELECT genre, ucase(substring(genre, 1, 3)) as code FROM imdb.genres GROUP BY genre LIMIT 100 FORMAT CSVWithNames" > seeds/genre_codes.csv -
Выполните команду
dbt seed. Это создаст новую таблицуgenre_codesв нашей базе данныхimdb_dbt(как задано в конфигурации схемы) со строками из нашего CSV-файла.clickhouse-user@clickhouse:~/imdb$ dbt seed 17:03:23 Running with dbt=1.1.0 17:03:23 Found 1 model, 0 tests, 1 snapshot, 0 analyses, 181 macros, 0 operations, 1 seed file, 6 sources, 0 exposures, 0 metrics 17:03:23 17:03:24 Concurrency: 1 threads (target='dev') 17:03:24 17:03:24 1 of 1 START seed file imdb_dbt.genre_codes..................................... [RUN] 17:03:24 1 of 1 OK loaded seed file imdb_dbt.genre_codes................................. [INSERT 21 in 0.65s] 17:03:24 17:03:24 Finished running 1 seed in 1.62s. 17:03:24 17:03:24 Completed successfully 17:03:24 17:03:24 Done. PASS=1 WARN=0 ERROR=0 SKIP=0 TOTAL=1 -
Подтвердите, что данные были загружены:
SELECT * FROM imdb_dbt.genre_codes LIMIT 10;+-------+----+ |genre |code| +-------+----+ |Drama |DRA | |Romance|ROM | |Short |SHO | |Mystery|MYS | |Adult |ADU | |Family |FAM | |Action |ACT | |Sci-Fi |SCI | |Horror |HOR | |War |WAR | +-------+----+=
Дополнительная информация
В предыдущих руководствах рассмотрены лишь базовые возможности dbt. Рекомендуем ознакомиться с отличной документацией dbt.