- Entender como as views e tabelas do projeto são materializadas no ClickHouse.
- Carregar dados com seeds e controlar os tipos do ClickHouse e o layout da tabela.
- Configurar um modelo de tabela com um motor do ClickHouse, sorting key e partitioning.
- Transformar uma tabela em um modelo incremental e escolher uma incremental strategy.
- Criar um snapshot.
- Usar visões materializadas do ClickHouse.
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 comdbt 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 pelodbt seed(raw_customers,raw_orders,raw_items,raw_products,raw_stores,raw_supplies).jaffle_shop(oschemado seu profile): seis views de staging (stg_*) e sete tabelas de mart (customers,orders,order_items,products,locations,supplies,metricflow_time_spine).
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.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 nodbt_project.yml: os models de staging são views e os marts são tables.
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:
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:
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 emseeds/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:
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.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 martorders é 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:
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:
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
Reconstruirorders 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:
unique_key: a coluna que identifica uma linha, aquiorder_id. O adapter a usa para substituir as linhas processadas novamente, em vez de duplicá-las.- Um filtro incremental: uma cláusula
whereenvolvida 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 comparaordered_atcom o valor mais recente já presente na tabela, referenciado pela variável{{ this }}.
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.
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:
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:- 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. - Uma table
orders__dbt_tmpé criada com a mesma structure deorders, e todas as rows deorderscujoorder_idnão está emorders__dbt_new_datasão copiadas para ela. - Todas as rows de
orders__dbt_new_datasão inseridas emorders__dbt_tmp. São os passos 2 e 3 que substituem as rows do dia mais recente em vez de duplicá-las. - É feito drop de
orders__dbt_new_data. orders__dbt_tmpé trocada comorderspor meio de um statement atômicoEXCHANGE TABLES(com um rename intermediário paraorders__dbt_backup), de modo queorderspassa a conter a nova versão.- É feito drop da versão antiga.
Estratégia append
A estratégiaappend 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:
orders é um único INSERT INTO jaffle_shop.orders ... SELECT ... com o SQL do model e o filter incremental, e ele gravou apenas uma linha.
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égiadelete+insert se baseia em exclusões leves e é configurada por meio do parâmetro incremental_strategy:
- Uma temporary table (
orders__dbt_new_data_<run_id>) é criada e as rows selecionadas pelo model são inseridas nela. - Um
DELETEé executado emorderspara cadaorder_idpresente na temporary table. - As rows da temporary table são inseridas em
orders. - A temporary table é removida.
Estratégia insert overwrite (experimental)
A estratégiainsert_overwrite substitui partições inteiras, portanto exige uma configuração partition_by, como a mensal em orders. Ela executa os seguintes passos:
- Cria uma staging table (
orders__dbt_new_data_<run_id>) com a mesma estrutura deorders. - Insere na staging table apenas as linhas selecionadas pelo modelo.
- Lista as partições presentes na staging table a partir de
system.parts. - Substitui exatamente essas partições em
orderscomALTER TABLE ... REPLACE PARTITION ... FROMa staging table. - Remove a staging table.
- É 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.
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 martcustomers é 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:
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:
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:
orders e customers reflitam o novo pedido e, em seguida, crie um segundo snapshot:
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:
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 umdbt 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:
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.
_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:
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.