Esta seção reúne guias para configurar o dbt e o adaptador do ClickHouse, além de um exemplo de uso do dbt com o ClickHouse usando um conjunto de dados público do IMDB. O exemplo abrange as seguintes etapas:
- Criar um projeto dbt e configurar o adaptador do ClickHouse.
- Definir um modelo.
- Atualizar um modelo.
- Criar um modelo incremental.
- Criar um modelo de snapshot.
- Usar VIEWs materializadas.
Esses guias foram elaborados para serem usados em conjunto com o restante da documentação, os recursos e configurações e a referência de materializações.
Configuração
Siga as instruções na seção Configuração do dbt e do adaptador ClickHouse para preparar seu ambiente.
Importante: o conteúdo a seguir foi testado com Python 3.9.
Prepare o ClickHouse
O dbt se destaca na modelagem de dados altamente relacionais. Para fins de exemplo, fornecemos um pequeno conjunto de dados do IMDB com o seguinte esquema relacional. Esse conjunto de dados vem do repositório de conjuntos de dados relacionais. Ele é simples em comparação com os esquemas normalmente usados com dbt, mas representa uma amostra gerenciável:

Usamos um subconjunto dessas tabelas, como mostrado.
Crie as tabelas a seguir:
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);Usamos a função s3 para ler os dados de origem a partir de endpoints públicos e inserir os dados. Execute os comandos a seguir para preencher as tabelas:
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');A execução dessas etapas pode variar dependendo da sua largura de banda, mas cada uma deve levar apenas alguns segundos para ser concluída. Execute a consulta a seguir para gerar um resumo de cada ator, em ordem pelo maior número de aparições em filmes, e confirmar que os dados foram carregados com sucesso:
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;A resposta deve ser assim:
+------+------------+----------+------------------+-------------+--------------+-------------------+
|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|
+------+------------+----------+------------------+-------------+--------------+-------------------+Nos guias posteriores, converteremos esta consulta em um modelo - materializando-o no ClickHouse como uma view e uma tabela no dbt.
Conectando ao ClickHouse
-
Crie um projeto dbt. Neste caso, damos a ele o nome da nossa source
imdb. Quando solicitado, selecioneclickhousecomo banco de dados de origem.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 -
Entre na pasta do seu projeto com
cd:cd imdb -
Neste ponto, você precisará de um editor de texto de sua preferência. Nos exemplos abaixo, usamos o popular VS Code. Ao abrir o diretório IMDB, você deverá ver uma coleção de arquivos yml e sql:

-
Atualize o arquivo
dbt_project.ymlpara especificar nosso primeiro modelo,actor_summary, e defina o profile comoclickhouse_imdb.

-
Em seguida, precisamos fornecer ao dbt os detalhes de conexão da nossa instância do ClickHouse. Adicione o seguinte ao arquivo
~/.dbt/profiles.yml.clickhouse_imdb: target: dev outputs: dev: type: clickhouse schema: imdb_dbt host: localhost port: 8123 user: default password: '' secure: FalseObserve que será necessário alterar o usuário e a senha. Há outras configurações disponíveis documentadas aqui.
-
No diretório IMDB, execute o comando
dbt debugpara confirmar se o dbt consegue se conectar ao 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!Confirme que a resposta inclui
Connection test: [OK connection ok], indicando que a conexão foi bem-sucedida.
Criando uma materialização de view simples
Ao usar a materialização de view, um modelo é recriado como uma view a cada execução, por meio de uma instrução CREATE VIEW AS no ClickHouse. Isso não requer armazenamento adicional de dados, mas as consultas serão mais lentas do que com materializações de tabela.
-
No diretório
imdb, exclua o diretóriomodels/example:clickhouse-user@clickhouse:~/imdb$ rm -rf models/example -
Crie um novo arquivo em
actors, dentro da pastamodels. Aqui, criamos arquivos, cada um representando um modelo de ator:clickhouse-user@clickhouse:~/imdb$ mkdir models/actors -
Crie os arquivos
schema.ymleactor_summary.sqlna pastamodels/actors.clickhouse-user@clickhouse:~/imdb$ touch models/actors/actor_summary.sql clickhouse-user@clickhouse:~/imdb$ touch models/actors/schema.ymlO arquivo
schema.ymldefine nossas tabelas. Depois, elas estarão disponíveis para uso em macros. Editemodels/actors/schema.ymlpara que contenha este conteúdo:version: 2 sources: - name: imdb tables: - name: directors - name: actors - name: roles - name: movies - name: genres - name: movie_directorsO
actors_summary.sqldefine o modelo propriamente dito. Observe que, na função config, também solicitamos que o modelo seja materializado como uma view no ClickHouse. Nossas tabelas são referenciadas no arquivoschema.ymlpor meio da funçãosource; por exemplo,source('imdb', 'movies')refere-se à tabelamoviesno banco de dadosimdb. Editemodels/actors/actors_summary.sqlpara que contenha este conteúdo:{{ 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_summaryObserve que incluímos a coluna
updated_atno nosso actor_summary final. Usamos isso mais tarde em materializações incrementais. -
No diretório
imdb, execute o comandodbt 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 -
O dbt representará o model como uma view no ClickHouse, conforme solicitado. Agora podemos consultar essa view diretamente. Essa view terá sido criada no banco de dados
imdb_dbt— isso é determinado pelo parâmetro schema no arquivo~/.dbt/profiles.yml, no perfilclickhouse_imdb.SHOW DATABASES;+------------------+ |name | +------------------+ |INFORMATION_SCHEMA| |default | |imdb | |imdb_dbt | <---criado pelo dbt! |information_schema| |system | +------------------+Ao consultar esta view, podemos reproduzir os resultados da nossa consulta anterior com uma sintaxe mais simples:
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| +------+------------+----------+------------------+------+---------+-------------------+
Criando uma materialização como tabela
No exemplo anterior, nosso modelo foi materializado como uma view. Embora isso possa oferecer desempenho suficiente para algumas consultas, instruções SELECT mais complexas ou consultas executadas com frequência podem ter melhor desempenho quando materializadas como tabela. Essa materialização é útil para modelos que serão consultados por ferramentas de BI, garantindo uma experiência mais rápida para os usuários. Na prática, isso faz com que os resultados da consulta sejam armazenados em uma nova tabela, com a sobrecarga de armazenamento correspondente — ou seja, um INSERT TO SELECT é executado. Observe que essa tabela será reconstruída todas as vezes, ou seja, não é incremental. Portanto, grandes conjuntos de resultados podem levar a tempos de execução longos — consulte Limitações do dbt.
-
Modifique o arquivo
actors_summary.sqlpara que o parâmetromaterializedseja definido comotable. Observe comoORDER BYé definido e note que usamos o mecanismo de tabelaMergeTree:{{ config(order_by='(updated_at, id, name)', engine='MergeTree()', materialized='table') }} -
No diretório
imdb, execute o comandodbt run. Essa execução pode levar um pouco mais de tempo — cerca de 10s na maioria das máquinas.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 -
Confirme a criação da tabela
imdb_dbt.actor_summary:SHOW CREATE TABLE imdb_dbt.actor_summary;Você deverá ver a tabela com os tipos de dados apropriados:
+---------------------------------------- |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) +---------------------------------------- -
Confirme que os resultados desta tabela são consistentes com as respostas anteriores. Observe a melhora perceptível no tempo de resposta agora que o modelo é uma tabela:
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| +------+------------+----------+------------------+------+---------+-------------------+Sinta-se à vontade para executar outras consultas nesse modelo. Por exemplo, quais atores têm os filmes com melhor classificação entre aqueles com mais de 5 aparições?
SELECT * FROM imdb_dbt.actor_summary WHERE num_movies > 5 ORDER BY avg_rank DESC LIMIT 10;
Criando uma materialização incremental
O exemplo anterior criou uma tabela para materializar o modelo. Essa tabela será reconstruída a cada execução do dbt. Isso pode ser inviável e extremamente custoso para grandes conjuntos de resultados ou transformações complexas. Para enfrentar esse desafio e reduzir o tempo de compilação, o dbt oferece materializações incrementais. Isso permite que o dbt insira ou atualize registros em uma tabela desde a última execução, tornando essa abordagem apropriada para dados no estilo de eventos. Nos bastidores, uma tabela temporária é criada com todos os registros atualizados e, em seguida, todos os registros inalterados, bem como os registros atualizados, são inseridos em uma nova tabela de destino. Isso resulta em limitações semelhantes para grandes conjuntos de resultados, assim como no modelo de tabela.
Para contornar essas limitações em grandes conjuntos, o adaptador oferece o modo 'inserts_only', no qual todas as atualizações são inseridas na tabela de destino sem criar uma tabela temporária (mais sobre isso abaixo).
Para ilustrar este exemplo, adicionaremos o ator "Clicky McClickHouse", que aparecerá em incríveis 910 filmes, garantindo que ele tenha aparecido em mais filmes até mesmo do que Mel Blanc.
-
Primeiro, modificamos nosso model para que ele seja do tipo incremental. Essa alteração exige:
- unique_key - Para garantir que o adaptador consiga identificar as linhas de forma única, precisamos fornecer uma unique_key — neste caso, o campo
idda nossa consulta será suficiente. Isso garante que não teremos linhas duplicadas na nossa tabela materializada. Para mais detalhes sobre restrições de unicidade, veja aqui. - Filtro incremental - Também precisamos informar ao dbt como ele deve identificar quais linhas foram alteradas em uma execução incremental. Isso é feito fornecendo uma expressão delta. Normalmente, isso envolve um timestamp para dados de evento; por isso, usamos nosso campo de timestamp updated_at. Essa coluna, que por padrão recebe o valor de now() quando as linhas são inseridas, permite identificar novos registros. Além disso, precisamos identificar o caso alternativo em que novos atores são adicionados. Usando a variável
{{this}}para representar a tabela materializada existente, chegamos à expressãowhere id > (select max(id) from {{ this }}) or updated_at > (select max(updated_at) from {{this}}). Incorporamos isso dentro da condição{% if is_incremental() %}, garantindo que ela seja usada apenas em execuções incrementais, e não quando a tabela é criada pela primeira vez. Para mais detalhes sobre a filtragem de linhas em modelos incrementais, veja esta discussão na documentação do dbt.
Atualize o arquivo
actor_summary.sqlda seguinte forma:{{ 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() %} -- este filtro será aplicado apenas em uma execução incremental where id > (select max(id) from {{ this }}) or updated_at > (select max(updated_at) from {{this}}) {% endif %}Observe que nosso model responderá apenas a atualizações e adições nas tabelas
roleseactors. Para responder a todas as tabelas, recomenda-se dividir este model em vários submodels, cada um com seus próprios critérios incrementais. Esses models, por sua vez, podem ser referenciados e conectados. Para mais detalhes sobre referências cruzadas entre models, veja aqui. - unique_key - Para garantir que o adaptador consiga identificar as linhas de forma única, precisamos fornecer uma unique_key — neste caso, o campo
-
Execute um
dbt rune confirme os resultados na tabela resultante: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| +------+------------+----------+------------------+------+---------+-------------------+ -
Agora vamos adicionar dados ao nosso modelo para ilustrar uma atualização incremental. Adicione nosso ator "Clicky McClickHouse" à tabela
actors:INSERT INTO imdb.actors VALUES (845466, 'Clicky', 'McClickHouse', 'M'); -
Vamos fazer com que "Clicky" estrele em 910 filmes aleatórios:
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; -
Confirme que ele agora é, de fato, o ator com mais aparições consultando diretamente a tabela de origem subjacente, sem passar por nenhum modelo do 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| +------+-------------------+----------+------------------+------+---------+-------------------+ -
Execute um
dbt rune confirme que nosso modelo foi atualizado e corresponde aos resultados acima: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| +------+-------------------+----------+------------------+------+---------+-------------------+
Aspectos internos
Podemos identificar as instruções executadas para realizar a atualização incremental acima consultando o log de consultas do 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;Ajuste a consulta acima ao período de execução. Deixamos a inspeção do resultado a cargo do usuário, mas destacamos a estratégia geral usada pelo adaptador para realizar atualizações incrementais:
- O adaptador cria uma tabela temporária
actor_sumary__dbt_tmp. As linhas alteradas são enviadas para essa tabela. - Uma nova tabela,
actor_summary_new,é criada. As linhas da tabela antiga, por sua vez, são enviadas da tabela antiga para a nova, com uma verificação para garantir que os IDs das linhas não existam na tabela temporária. Isso lida de forma eficaz com atualizações e duplicatas. - Os resultados da tabela temporária são enviados para a nova tabela
actor_summary: - Por fim, a nova tabela é trocada atomicamente com a versão antiga por meio de uma instrução
EXCHANGE TABLES. A tabela antiga e a temporária são então removidas.
Isso é ilustrado abaixo:

Essa estratégia pode apresentar desafios em modelos muito grandes. Para mais detalhes, consulte Limitações.
Estratégia Append (modo apenas inserções)
Para contornar as limitações de grandes conjuntos de dados em modelos incrementais, o adaptador usa o parâmetro de configuração do dbt incremental_strategy. Ele pode ser definido com o valor append. Quando isso é feito, as linhas atualizadas são inseridas diretamente na tabela de destino (também chamada de imdb_dbt.actor_summary) e nenhuma tabela temporária é criada.
Observação: o modo append-only exige que seus dados sejam imutáveis ou que duplicatas sejam aceitáveis. Se você quiser um modelo de tabela incremental com suporte a linhas alteradas, não use este modo!
Para ilustrar este modo, vamos adicionar mais um ator novo e executar novamente o dbt run com incremental_strategy='append'.
-
Configure o modo append-only em actor_summary.sql:
{{ config(order_by='(updated_at, id, name)', engine='MergeTree()', materialized='incremental', unique_key='id', incremental_strategy='append') }} -
Vamos adicionar mais um ator famoso: Danny DeBito
INSERT INTO imdb.actors VALUES (845467, 'Danny', 'DeBito', 'M'); -
Vamos colocar Danny no elenco de 920 filmes aleatórios.
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; -
Execute um dbt run e confirme que Danny foi adicionado à tabela actor_summary
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, 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| +------+-------------------+----------+------------------+------+---------+-------------------+
Observe como essa execução incremental foi muito mais rápida do que a inserção de "Clicky".
Ao verificar novamente a tabela query_log, vemos as diferenças entre as 2 execuções incrementais:
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
-- este filtro só será aplicado em uma execução incremental
WHERE id > (SELECT max(id) FROM imdb_dbt.actor_summary) OR updated_at > (SELECT max(updated_at) FROM imdb_dbt.actor_summary)Nesta execução, apenas as novas linhas são adicionadas diretamente à tabela imdb_dbt.actor_summary, sem envolver a criação de tabela.
Modo de exclusão e inserção (experimental)
Historicamente, o ClickHouse oferecia apenas suporte limitado a atualizações e exclusões, na forma de mutações assíncronas. Elas podem exigir uso de E/S extremamente intensivo e, em geral, devem ser evitadas.
O ClickHouse 22.8 introduziu as exclusões leves, e o ClickHouse 25.7 introduziu as atualizações leves. Com a introdução dessas funcionalidades, as alterações feitas por consultas de atualização individuais, mesmo quando materializadas de forma assíncrona, serão refletidas instantaneamente para o usuário.
Esse modo pode ser configurado para um modelo por meio do parâmetro incremental_strategy, ou seja.
{{ config(order_by='(updated_at, id, name)', engine='MergeTree()', materialized='incremental', unique_key='id', incremental_strategy='delete+insert') }}Essa estratégia opera diretamente na tabela do modelo de destino, portanto, se houver algum problema durante a operação, os dados no modelo incremental provavelmente ficarão em um estado inválido — não há atualização atômica.
Em resumo, esta abordagem:
- O adaptador cria uma tabela temporária
actor_sumary__dbt_tmp. As linhas alteradas são gravadas nessa tabela. - Um
DELETEé executado na tabelaactor_summaryatual. As linhas são excluídas por id com base emactor_sumary__dbt_tmp - As linhas de
actor_sumary__dbt_tmpsão inseridas emactor_summaryusandoINSERT INTO actor_summary SELECT * FROM actor_sumary__dbt_tmp.
Esse processo é mostrado abaixo:

modo insert_overwrite (experimental)
Executa as seguintes etapas:
- Crie uma tabela de staging (temporária) com a mesma estrutura da relação do modelo incremental:
CREATE TABLE {staging} AS {target}. - Insira apenas os novos registros (produzidos por SELECT) na tabela de staging.
- Substitua apenas as novas partições (presentes na tabela de staging) na tabela de destino.
Essa abordagem tem as seguintes vantagens:
- É mais rápida do que a estratégia padrão porque não copia a tabela inteira.
- É mais segura do que outras estratégias porque não modifica a tabela original até que a operação INSERT seja concluída com sucesso: em caso de falha intermediária, a tabela original não é modificada.
- Implementa a prática recomendada de engenharia de dados de "imutabilidade das partições", o que simplifica o processamento de dados incremental e paralelo, rollbacks etc.

Criando um snapshot
Os snapshots do dbt permitem registrar, ao longo do tempo, as alterações em um modelo mutável. Isso, por sua vez, permite consultas em um ponto específico no tempo sobre os modelos, em que analistas podem "voltar no tempo" para ver o estado anterior de um modelo. Isso é feito usando dimensões de mudança lenta do tipo 2, nas quais colunas de data inicial e final registram quando uma linha era válida. Essa funcionalidade é compatível com o adaptador do ClickHouse e é demonstrada abaixo.
Este exemplo pressupõe que você concluiu Criando um modelo de tabela incremental. Certifique-se de que seu actor_summary.sql não defina inserts_only=True. Seu models/actor_summary.sql deve ficar assim:
{{ 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() %}
-- este filtro só será aplicado em uma execução incremental
where id > (select max(id) from {{ this }}) or updated_at > (select max(updated_at) from {{this}})
{% endif %}-
Crie um arquivo
actor_summaryno diretório snapshots.touch snapshots/actor_summary.sql -
Atualize o conteúdo do arquivo actor_summary.sql com o conteúdo a seguir:
{% snapshot actor_summary_snapshot %} {{ config( target_schema='snapshots', unique_key='id', strategy='timestamp', updated_at='updated_at', ) }} select * from {{ref('actor_summary')}} {% endsnapshot %}
Algumas observações sobre esse conteúdo:
- A consulta
selectdefine os resultados que você deseja capturar em snapshots ao longo do tempo. A função ref é usada para referenciar o modelo actor_summary que criamos anteriormente. - Precisamos de uma coluna de timestamp para indicar alterações nos registros. Nossa coluna updated_at (consulte Criando um modelo de tabela incremental) pode ser usada aqui. O parâmetro strategy indica que usamos um timestamp para marcar atualizações, e o parâmetro updated_at especifica qual coluna usar. Se ele não estiver presente no seu modelo, você também pode usar a estratégia check. Isso é significativamente menos eficiente e exige que o usuário especifique uma lista de colunas para comparação. O dbt compara os valores atuais e históricos dessas colunas, registrando quaisquer alterações (ou não fazendo nada se forem idênticos).
-
Execute o comando
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
Observe que foi criada uma tabela actor_summary_snapshot no banco de dados snapshots (definido pelo parâmetro target_schema).
-
Ao examinar uma amostra desses dados, você verá como o dbt incluiu as colunas dbt_valid_from e dbt_valid_to. Esta última tem valores definidos como NULL. Nas execuções seguintes, isso será atualizado.
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 | +------+----------+------------+----------+-------------------+------------+ -
Faça nosso ator favorito, Clicky McClickHouse, aparecer em outros 10 filmes.
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; -
Execute novamente o comando
dbt runa partir do diretórioimdb. Isso atualizará o modelo incremental. Quando isso for concluído, execute odbt snapshotpara capturar as alterações.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 -
Se agora consultarmos nosso snapshot, observe que temos 2 linhas para Clicky McClickHouse. Nossa entrada anterior agora tem um valor em dbt_valid_to. Nosso novo valor é registrado com o mesmo valor na coluna dbt_valid_from e com dbt_valid_to igual a null. Se tivéssemos novas linhas, elas também seriam adicionadas ao snapshot.
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 | +------+----------+------------+----------+-------------------+-------------------+
Para mais detalhes sobre os snapshots do dbt, veja aqui.
Usando seeds
O dbt permite carregar dados de arquivos CSV. Esse recurso não é adequado para carregar grandes exportações de um banco de dados; ele foi projetado mais para arquivos pequenos, normalmente usados para tabelas de códigos e dicionários, por exemplo, para mapear códigos de países para nomes de países. Neste exemplo simples, geramos e depois carregamos uma lista de códigos de gênero usando a funcionalidade de seed.
-
Geramos uma lista de códigos de gênero a partir do nosso conjunto de dados existente. No diretório do dbt, use o
clickhouse-clientpara criar um arquivoseeds/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 -
Execute o comando
dbt seed. Isso criará uma nova tabelagenre_codesno nosso banco de dadosimdb_dbt(conforme definido na nossa configuração de schema) com as linhas do nosso arquivo 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 -
Confirme que os dados foram carregados:
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 | +-------+----+=
Mais informações
Os guias anteriores apenas mostram uma pequena parte das funcionalidades do dbt. Recomenda-se que os usuários leiam a excelente documentação do dbt.