Skip to main content
Este guia aborda os aspectos específicos do ClickHouse no dbt usando o projeto Jaffle Shop for ClickHouse, a adaptação para ClickHouse do clássico projeto de exemplo da dbt Labs. Partindo de um projeto que já é compilado com sucesso, ele mostra como:
  1. Entender como as views e tabelas do projeto são materializadas no ClickHouse.
  2. Carregar dados com seeds e controlar os tipos do ClickHouse e o layout da tabela.
  3. Configurar um modelo de tabela com um motor do ClickHouse, sorting key e partitioning.
  4. Transformar uma tabela em um modelo incremental e escolher uma incremental strategy.
  5. Criar um snapshot.
  6. Usar visões materializadas do ClickHouse.
Ele foi elaborado para ser lido junto com o restante da documentação, a página de features and configurations e a referência de materializations.

Antes de começar

Siga primeiro o README de ClickHouse/jaffle-shop-clickhouse. Ele explica como configurar o projeto com dbt Core 1.x, dbt OSS, dbt v2 ou a plataforma dbt, como apontá-lo para um ClickHouse local (docker) ou para o ClickHouse Cloud, como carregar os dados de exemplo com dbt seed e como executar o primeiro dbt build. Depois que o dbt build for concluído com sucesso, volte para cá para ver os exemplos e as configurações específicas do ClickHouse. Após as etapas do README, você deverá ter dois bancos de dados no ClickHouse:
  • raw: as seis tabelas de origem carregadas a partir de CSV files pelo dbt seed (raw_customers, raw_orders, raw_items, raw_products, raw_stores, raw_supplies).
  • jaffle_shop (o schema do seu profile): seis views de staging (stg_*) e sete tabelas de mart (customers, orders, order_items, products, locations, supplies, metricflow_time_spine).
Se o seu profile usar um schema diferente, substitua jaffle_shop nas consultas abaixo pelo seu valor.
dbt Core 1.x, dbt OSS, dbt v2 e a plataforma dbt. Todos os comandos e models deste guia são idênticos em todos eles. Os exemplos foram testados com dbt Core 1.12 e dbt-clickhouse 1.10, e com dbt OSS 2.0, no ClickHouse 26.8; o dbt v2 executa o mesmo adapter, e a plataforma dbt executa o dbt v2. A saída de console exibida é do dbt Core 1.x, e os poucos pontos em que os motores se comportam de maneira diferente estão indicados. Consulte a página do dbt OSS, dbt v2 e plataforma dbt para saber o status atual do adapter v2 e Connect ClickHouse na documentação do dbt para começar a usar a plataforma dbt.
Todas as instruções SQL que não são comandos dbt devem ser executadas diretamente no ClickHouse, por exemplo com o clickhouse client, com o SQL console do ClickHouse Cloud ou com o cliente SQL de sua preferência.

Como o projeto é materializado

O Jaffle Shop configura suas materializations no dbt_project.yml: os models de staging são views e os marts são tables.
Um model do tipo view é reconstruído com um statement CREATE OR REPLACE VIEW a cada execução. Ele não armazena dados, portanto seu build não tem custo algum, mas toda consulta sobre ele executa o SQL do model nas source tables. O ClickHouse mantém o SQL compilado do model na view definition:
Um model do tipo table é reconstruído do zero a cada execução: o adapter cria uma nova table, executa um INSERT INTO ... SELECT com o SQL do model e a troca atomicamente pela versão anterior. O desempenho das consultas é muito melhor do que o de uma view, ao custo de armazenamento e da reconstrução da table inteira a cada execução. Veja a table que o dbt criou para o mart orders:
Duas coisas aqui são específicas do ClickHouse. O model não declara um motor de tabela, então o adapter usa MergeTree, e também não declara uma sorting key, então o adapter usa ORDER BY tuple(), ou seja, os dados não são ordenados de forma alguma. Isso é aceitável para um projeto de exemplo, mas, em uma table real, você vai querer definir ambos — que é justamente o que as próximas seções fazem. A página de materializations lista todas as configurações de table compatíveis com o adapter.

Carregando dados com seeds

O Jaffle Shop usa seeds do dbt para carregar seus dados brutos a partir dos arquivos CSV em seeds/jaffle-data. Os seeds servem para dados de referência pequenos e estáticos (tabelas de códigos, mapeamentos), e não para carregar um data warehouse; o projeto os utiliza por conveniência, para que você possa começar sem precisar de outra ferramenta de ingestão — e é por isso que os seeds ficam desabilitados a menos que você passe --vars '{"load_source_data": true}'. Mesmo assim, os seeds são um bom ponto de partida para entender como o dbt cria tabelas no ClickHouse. O dbt infere um tipo de coluna para cada coluna do CSV, e os tipos inferidos variam entre os motores: Quando o tipo importa, defina-o explicitamente com column_types. O projeto já faz isso para a coluna opened_at do seed raw_stores em dbt_project.yml:
Os seeds também aceitam as configurações de tabela do ClickHouse engine, order_by e partition_by. Por exemplo, para ordenar o seed raw_orders pelo horário do pedido e particioná-lo por mês, adicione um arquivo de propriedades junto aos CSVs, seeds/jaffle-data/_raw_orders.yml:
Use um arquivo de propriedades para essas configurações de seed do ClickHouse em vez das chaves +order_by ou +engine sob seeds: no dbt_project.yml. O dbt Core 1.x aceita ambas as formas, mas o dbt v2 só as reconhece em um arquivo de propriedades e rejeita as chaves do dbt_project.yml com Unrecognized key ... Custom keys must go under +meta.
Recarregue esse seed e verifique a tabela que ele gerou:
dbt seed --full-refresh remove e recria a tabela, portanto execute-o antes de criar qualquer objeto que dependa diretamente dos dados da tabela, como a visão materializada apresentada mais adiante neste guia.

Configurando uma tabela para o ClickHouse

O mart orders é o ponto de partida natural: ele é consultado pelo mart customers e pelas métricas do projeto, além de ser uma tabela em estilo de eventos com um timestamp. Adicione um bloco config no início de models/marts/orders.sql para escolher o motor, a chave de ordenação e um esquema de particionamento:
O restante do model permanece como está. materialized='table' repete o que o dbt_project.yml já define para as marts, o que mantém o model autodescritivo quando você o alterar para incremental mais adiante. Reconstrua apenas este model:
A tabela agora possui uma sorting key adequada e uma partição por mês:
Além de engine, order_by e partition_by, os models de tabela aceitam primary_key, ttl, settings, query_settings, projections e indexes, e as colunas podem levar codec e ttl por meio de um contrato de model. Todos eles estão descritos na página de materializations.

Criando um modelo incremental

Reconstruir orders do zero a cada execução é aceitável para 62.000 linhas, mas não para uma tabela que cresce milhões de linhas por dia. A materialização incremental do dbt processa apenas as linhas que mudaram desde a última execução. Converter o modelo orders exige duas adições:
  1. unique_key: a coluna que identifica uma linha, aqui order_id. O adapter a usa para substituir as linhas processadas novamente, em vez de duplicá-las.
  2. Um filtro incremental: uma cláusula where envolvida por {% if is_incremental() %} que seleciona apenas as linhas a processar. Ela é aplicada nas execuções incrementais, mas não quando a tabela é criada pela primeira vez (ou reconstruída com --full-refresh). Os pedidos possuem um timestamp, portanto o filtro compara ordered_at com o valor mais recente já presente na tabela, referenciado pela variável {{ this }}.
Atualize models/marts/orders.sql para que o bloco config e o final do modelo fiquem assim:
stg_orders trunca ordered_at para o dia, então o filtro usa >=: em cada execução, todo o dia mais recente é processado novamente e, graças a unique_key, as linhas já carregadas são substituídas em vez de duplicadas. É isso que torna a abordagem segura para pedidos que chegam mais tarde no mesmo dia. Execute o model. A table já existe, portanto esta primeira execução já é incremental: apenas o dia mais recente é reprocessado.
Agora adicione alguns dados novos. Os dados da Jaffle Shop terminam em agosto de 2025, então vamos incluir um novo cliente, Clicky McClickHouse, que pediu um jaffle ontem. Insira um cliente, um pedido e o item desse pedido nas tabelas raw:
O id da loja é Philadelphia, o item é um jaffle nutellaphone who dis? a 11,00 e o imposto é o de Philadelphia, de 6%, portanto os testes de dados do projeto continuam passando. Execute o projeto inteiro para que as views de staging e a tabela order_items vejam as novas linhas antes de orders:
O novo pedido está na tabela incremental e o mart customers, reconstruído a partir dela, já reconhece o novo cliente:

Internals

O query log do ClickHouse mostra as instruções que o adapter executou para a atualização incremental:
A estratégia incremental padrão do adapter funciona da seguinte forma. Nos diagramas desta seção, uma seta de uma table para um statement significa que o statement lê essa table; uma seta de um statement para uma table significa que ele grava, aplica mutação, renomeia ou faz drop nela:
  1. Uma table orders__dbt_new_data é criada e o SQL do model, incluindo o filter incremental, é inserido nela. Na execução acima, 378 rows foram gravadas: os 377 pedidos do dia mais recente já carregados mais o novo.
  2. Uma table orders__dbt_tmp é criada com a mesma structure de orders, e todas as rows de orders cujo order_id não está em orders__dbt_new_data são copiadas para ela.
  3. Todas as rows de orders__dbt_new_data são inseridas em orders__dbt_tmp. São os passos 2 e 3 que substituem as rows do dia mais recente em vez de duplicá-las.
  4. É feito drop de orders__dbt_new_data.
  5. orders__dbt_tmp é trocada com orders por meio de um statement atômico EXCHANGE TABLES (com um rename intermediário para orders__dbt_backup), de modo que orders passa a conter a nova versão.
  6. É feito drop da versão antiga.
O passo 2 copia a tabela inteira, portanto essa estratégia é tão custosa quanto reconstruir a tabela em modelos muito grandes; consulte as limitações. As estratégias abaixo evitam essa cópia.

Estratégia append

A estratégia append insere as linhas selecionadas pelo model diretamente na target table. Nenhuma temporary table é criada e nada é copiado, portanto é o mais barato que uma execução incremental pode ser. Em troca, nada é deduplicado: se o filter incremental selecionar uma linha que já está na table, ela aparecerá duas vezes. Use-a para dados imutáveis, no estilo de eventos, e garanta que o filter selecione apenas linhas genuinamente novas. Com o ordered_at truncado por dia, isso significa mudar o filter para >. Altere o model:
Adicione um segundo cliente novo, Danny DeBito, com um pedido feito hoje no Brooklyn (4% de imposto) contendo um jaffle e um café:
O modelo incremental foi executado em uma fração do tempo da execução anterior. Os dois novos clientes têm exatamente um pedido na tabela:
O query log confirma a diferença: desta vez, o único statement que toca orders é um único INSERT INTO jaffle_shop.orders ... SELECT ... com o SQL do model e o filter incremental, e ele gravou apenas uma linha.
Com > e um timestamp truncado por dia, um pedido que chega mais tarde no mesmo dia do pedido carregado mais recente nunca é capturado. Em um projeto real, aplique o filter sobre um timestamp com full precision ou sobre um horário de ingestão monotonicamente crescente ao usar a strategy append.

Estratégia de delete e insert

Historicamente, o ClickHouse ofereceu suporte apenas limitado a atualizações e exclusões, na forma de mutações assíncronas. Elas podem consumir muita E/S e, de modo geral, devem ser evitadas. O ClickHouse 22.8 introduziu as exclusões leves e o ClickHouse 25.7 introduziu as atualizações leves. Com elas, o efeito de uma única instrução de exclusão ou atualização fica visível imediatamente da perspectiva do usuário, ainda que seja materializado de forma assíncrona. A estratégia delete+insert se baseia em exclusões leves e é configurada por meio do parâmetro incremental_strategy:
Ela atua diretamente na target table, portanto, se algo falhar no meio do processo, os dados do incremental model provavelmente ficarão em um estado inválido: não há swap atômico. Em resumo:
  1. Uma temporary table (orders__dbt_new_data_<run_id>) é criada e as rows selecionadas pelo model são inseridas nela.
  2. Um DELETE é executado em orders para cada order_id presente na temporary table.
  3. As rows da temporary table são inseridas em orders.
  4. A temporary table é removida.

Estratégia insert overwrite (experimental)

A estratégia insert_overwrite substitui partições inteiras, portanto exige uma configuração partition_by, como a mensal em orders. Ela executa os seguintes passos:
  1. Cria uma staging table (orders__dbt_new_data_<run_id>) com a mesma estrutura de orders.
  2. Insere na staging table apenas as linhas selecionadas pelo modelo.
  3. Lista as partições presentes na staging table a partir de system.parts.
  4. Substitui exatamente essas partições em orders com ALTER TABLE ... REPLACE PARTITION ... FROM a staging table.
  5. Remove a staging table.
Essa abordagem tem as seguintes vantagens:
  • É mais rápida que a estratégia padrão, pois não copia a tabela inteira.
  • É mais segura que as outras estratégias, pois 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 permanece intacta.
  • Implementa a boa prática de engenharia de dados de “imutabilidade de partições”, que simplifica o processamento incremental e paralelo de dados, rollbacks etc.
A página de materializations aborda as demais opções da materialization incremental, incluindo a strategy microbatch e o on_schema_change.

Criando um snapshot

Os snapshots do dbt registram como as linhas de uma tabela mutável mudam ao longo do tempo, permitindo que analistas consultem o estado dos dados em qualquer momento do passado. Eles implementam dimensões de variação lenta do tipo 2: cada versão de uma linha é armazenada com o intervalo durante o qual ela foi válida. O mart customers é um bom candidate: count_lifetime_orders, lifetime_spend e customer_type mudam sempre que um cliente faz um novo pedido. Antes de continuar, volte o model orders para a incremental strategy padrão da seção incremental (remova incremental_strategy='append' e altere o filter de volta para >=), para que os pedidos feitos mais tarde no dia de hoje sejam capturados. Desde o dbt 1.9, os snapshots são definidos em YAML. Crie snapshots/customers_snapshot.yml:
A strategy check compara as colunas listadas entre o current snapshot e o source a cada execução e registra uma nova version sempre que alguma delas mudar. Se o seu model tiver uma coluna de timestamp confiável de “última atualização”, a strategy timestamp é mais econômica: defina strategy: timestamp e updated_at: <column>. O last_ordered_at do Jaffle Shop é truncado para o dia, portanto não detectaria um segundo pedido no mesmo dia — e é por isso que este exemplo usa check. Crie o primeiro snapshot:
A tabela de snapshot é criada junto aos models. A macro generate_schema_name do projeto coloca cada relation no schema de destino para targets que não sejam de production, portanto uma config schema no snapshot só teria efeito com o target prod. Ela contém uma linha por cliente, com as colunas de controle do dbt dbt_valid_from e dbt_valid_to; esta última é NULL para a versão atual de uma linha:
Hoje o Clicky volta para tomar um café:
Execute os models para que orders e customers reflitam o novo pedido e, em seguida, crie um segundo snapshot:
Clicky agora tem duas linhas no snapshot. A primeira versão foi fechada com a definição do seu dbt_valid_to, e a nova versão, agora um cliente returning com dois pedidos, está aberta. Danny não mudou, portanto sua linha permanece intacta:
Internamente, o adapter constrói a nova versão do snapshot em uma table customers_snapshot__snapshot_upsert e a coloca em uso com EXCHANGE TABLES (ou por meio de um drop e rename, nos casos em que o servidor não consegue trocar tables), de modo que os leitores veem ou a versão anterior ou a nova versão do snapshot. Consulte a seção sobre snapshot na página de materializations para a referência de configuração.

Usando visões materializadas

Tudo o que vimos até aqui exige um dbt run para trazer novos dados para os models. As visões materializadas do ClickHouse funcionam de outra forma: elas são gatilhos de insert. Cada bloco de linhas inserido na tabela de origem é transformado pelo SELECT da view e gravado em uma tabela de destino, sem nenhum agendamento envolvido. O adapter as expõe por meio da materialization materialized_view. Crie models/marts/daily_store_revenue.sql com o número de pedidos e a receita por loja e por dia, lendo diretamente da tabela de pedidos brutos:
O engine e o order_by se aplicam à tabela de destino. O SummingMergeTree soma as colunas numéricas das linhas que compartilham a mesma sorting key ao mesclar partes, que é exatamente o que uma agregação por dia e por loja precisa.
O adapter criou dois objetos: a target table, nomeada a partir do model, e a própria visão materializada com o suffix _mv, apontando para a target table por meio de uma cláusula TO. Por padrão (catchup=True), a target table também recebeu o backfill dos pedidos existentes:
Agora insira outro pedido bruto para o Danny, sem executar o dbt em seguida:
A target table já reflete isso. Brooklyn agora tem dois pedidos hoje:
A consulta agrega com sum() e GROUP BY de propósito: o SummingMergeTree só colapsa linhas com a mesma chave quando as partes são mescladas em segundo plano, portanto, até que isso aconteça, os dois pedidos de Brooklyn são duas linhas na tabela. Sempre agregue na leitura (ou use FINAL) com motores de soma e agregação. Enquanto isso, o model incremental orders continua com um único pedido para Danny até o próximo dbt run. Execuções posteriores de dbt run preservam a target table e seus dados e apenas atualizam a view definition, com ALTER TABLE ... MODIFY QUERY quando a mudança permite, de modo que é seguro manter o model no projeto. O dbt run --full-refresh reconstrói a target table e faz o backfill novamente (a menos que catchup seja False). A página de visões materializadas cobre o restante: schema changes com on_schema_change, desativação do backfill com catchup, visões materializadas atualizáveis, várias views alimentando o mesmo target e a definição da target table como um model próprio.

Mais informações

Este guia apenas arranha a superfície do dbt. A documentação do dbt é a referência para tudo que não seja específico do ClickHouse. Quanto ao adapter, consulte a página features and configurations para configurações de profile e features globais, a página de materializations para cada configuration usada acima e a página do dbt OSS, dbt v2 e plataforma dbt caso você utilize o dbt OSS, o dbt v2 ou a plataforma dbt. Contribuições com novos exemplos para o Jaffle Shop for ClickHouse são bem-vindas.
Última modificação em 26 de setembro de 2026