Guia prático do DBT: Organizando as pastas do projeto.

Uma jornada em 3 etapas para dominar a ferramenta de transformação de dados — DBT — Parte 2 de 3.

Alice Thomaz
10 min readAug 27, 2023

Este é o segundo de uma série de artigos com o objetivo de guiar e proporcionar mais informações sobre o DBT, uma ferramenta muito usada no mercado atual para transformação de dados. Neste artigo, vamos explicar como organizamos as pastas principais do nosso projeto e qual o papel de cada uma. Caso queira ler a primeira parte do guia, é só acessar o link: Desvendando a arquitetura e configuração inicial. E se quiser se aprofundar ainda mais na organização de um projeto DBT, recomendo dar uma olhada na documentação oficial da ferramenta: About dbt projects.

>>> Organização do repositório

Após a configuração da máquina local, é hora de abrir o VSCODE. A partir daí, você entra no container que foi criado na parte 1 do guia e dá início à construção da estrutura inicial do projeto. Essa estrutura, posteriormente, será compartilhada com os demais membros da equipe por meio de um repositório à sua escolha, como o Github ou Gitlab. Abaixo, listamos algumas pastas e arquivos cruciais, que serão detalhados ao longo deste artigo.

Data

Pasta para armazenamento dos Seeds.

Seeds são arquivos em CSV dentro do projeto DBT, que são carregados no Data Warehouse usando o comando dbt seed. Eles podem ser referenciados em outras modelagens normalmente usando a função ref — {{ ref(‘table_name’) }}.

Pelos arquivos CSV estarem alocados dentro do repositório do DBT, eles são atualizados via controle de versão. Por conta disso são mais bem utilizados para dados estáticos que não precisam de alteração constante.

Exemplo de bons usos para seeds:

  • Lista de códigos DDI X os nomes dos países.
  • Lista de emails teste que precisam ser removidos das análises.
  • Lista com os APP ID para separação de BU e modalidade dos eventos.

Exemplo de mau uso para seeds:

  • Carregar dados raw que foram exportados para CSV.
  • Qualquer dado de produção contendo informação sensível.

Abaixo o exemplo de um arquivo armazenado nessa pasta:
data/custom_dvce.csv

os_family,dvce_type
iOS,mobile
Android,mobile
BlackBerry Tablet OS,mobile
Windows,desktop
FreeBSD,desktop
Ubuntu,desktop


Packages

Os engenheiros de software frequentemente modularizam uma série de códigos em bibliotecas que auxiliam a comunidade a se concentrar mais na lógica particular de seus negócios ao invés de perder tempo implementando funções que outras pessoas já gastaram tempo aperfeiçoando.

No dbt, bibliotecas como essas são chamadas de packages. Os packages do dbt são muito uteis porque muitos dos problemas analíticos que encontramos são compartilhados entre as organizações, por exemplo:

  • Transformar dados do Snowplow em tabelas categorizadas.
  • Macros para trabalhar com dados carregados do Stitch.
  • Pacotes que registram dados de execução de modelagens.

Os packages dbt são, na verdade, projetos dbt autônomos, com modelos e macros que abordam uma área de problema específica. Como usuário do dbt, ao adicionar um pacote ao seu projeto, os modelos e macros do pacote se tornarão parte do seu próprio projeto.

Para usa-los é preciso identificar o nome e versão do pacote no arquivo packages.yml, após isso deve-se rodar o comando dbt deps para instala-los localmente. Ao instalar, as informações do pacote vão passar a constar na pasta dbt_packages. Esse diretório é ignorado pelo git para não gerar sources duplicados, então sempre que alguém for trabalhar pela primeira vez no projeto deverá rodar o comando.

Exemplo de um arquivo packages.yml com os principais pacotes que usamos hoje:

packages:
- package: calogica/dbt_expectations
version: [">=0.3.0", "<0.6.0"]
- package: dbt-labs/dbt_external_tables
version: 0.8.4
- package: elementary-data/elementary
version: 0.7.6

Macros

No DBT você consegue combinar SQL com Jinja, com essa linguagem você consegue transformar seu projeto dbt e ter a capacidade de fazer coisas que normalmente não são possíveis em SQL. Por exemplo, com Jinja você pode:

  • Usar estruturas de controle (por exemplo, instruções if e loops for) em SQL.
  • Usar variáveis de ambiente em seu projeto dbt para deploys em produção.
  • Criar trechos abstratos de SQL em macros reutilizáveis — são análogos a funções na maioria das linguagens de programação.

Macros no Jinja são pedaços de código que podem ser reutilizados múltiplas vezes, eles são definidos em um arquivo .sql e armazenados nessa pasta macros. Esses arquivos podem conter 1 ou mais macros.

Exemplo:

  • Macro:
{% macro parse_dt_string(column) %}
CASE WHEN {{ column }} IS NOT NULL
THEN TIMESTAMP 'epoch' + {{ column }} * INTERVAL '1 second'
ELSE NULL
END
{% endmacro %}
  • Modelagem:
SELECT
{{ parse_dt_string('time') }} AS t_time,
id as payment_id,

FROM braze_data.notifications

É possível também usar macros de pacotes prontos como mostrado no tópico anterior, elas são agrupadas em pacotes online que podem ser instalados localmente para uso. Sempre ao usar numa modelagem lembrar de adicionar o nome do pacote como prefixo. Exemplo de um pacote muito popular, o dbt-utils:

{{ dbt_utils.group_by(5) }}

Models

Os modelos são onde os desenvolvedores passam a maior parte do tempo em um ambiente dbt. Os modelos são escritos principalmente como uma instrução select e salvos como um arquivo .sql. Nós separamos os modelos nos tipos sources, marts e staging.

>Sources: Eles possibilitam nomear e descrever os dados carregados em seu warehouse por suas ferramentas de extract e load. Ao declarar essas tabelas como fontes no dbt, você pode passar a chamar elas em suas modelagens analíticas. As pastas são compostas sempre por um arquivo de documentação (schema.yml) e um de identificação do source (sources.yml), esse segundo serve para identificar o schema e tabela no Redshift.
Exemplo:

version: 2

sources:
- name: source_name
schema: schema_name
loader: DMS

freshness:
warn_after: { count: 12, period: hour }
error_after: { count: 24, period: hour }
loaded_at_field: tstamp_column

tables:
- name: table_1
- name: table_2

Os dados sources costumam ser de responsabilidade do time de engenharia e aqui no Pilar 2 da Afya separamos também em dois subgrupos:

  • RAW: São dados brutos que apenas fazemos a chamada deles no DBT e adaptamos a nomenclatura das colunas e tabela, para referencia-lo deve usar a condição: {{ source(‘source_name’, ‘table_name’) }}
{{
config(
materialized = 'incremental',
full_refresh = false,
unique_key = 'column1',
dist = 'column1',
sort = 'column2',
)
}}

WITH source AS(

SELECT
column1 AS tn_column1,
column2 AS tn_column2,
column3 AS tn_column3,
column4 AS tn_column4
FROM {{ source('source_name', 'table_name') }}
WHERE 1=1
{% if target.name == 'dev' %}
AND column2 >= DATEADD(DAY, -{{var('testing_days')}}, CURRENT_DATE)
{% elif is_incremental() %}
AND column2::DATE >= DATEADD(DAY, -{{var('filter_days_raw')}}, (SELECT MAX(tn_column2::DATE) FROM {{ this }}))
{% endif %}

)

SELECT
tn_column1,
tn_column2,
tn_column3,
tn_column4
FROM source
  • Custom: Por termos dados de eventos muito pesados, foram criados modelos custom para deixar os dados mais nichados por BU e modalidade e com isso deixar eles mais leves para uso nos modelos marts. Com isso tivemos um ganho significativo de performance.

>Marts: Na pasta marts temos as modelagens analíticas em .sql. Aqui elas são de responsabilidade do time de análise e ciências e são repartidas por periodicidade de atualização e áreas de negócio, elas usam como fonte as tabelas criadas na pasta das sources via condição: {{ ref(‘table_name’) }}.

{{
config(
materialized = 'incremental',
full_refresh = false,
unique_key = 'column1'
dist = 'column1',
sort = 'column2',
)
}}

WITH cte_1 AS(

SELECT
column1,
column2,
column3,
column4
FROM {{ ref('model_name') }}
{% if is_incremental() %}
WHERE column2::DATE >= DATEADD(DAY, -7, CURRENT_DATE)
{% endif %}

)

...

Todas as pastas também possuem um arquivo de documentação (schema.yml). Abaixo um exemplo do arquivo:

version: 2

models:
- name: nome_do_modelo
description: descrição da tabela.
columns:
- name: coluna_1
description: descrição da coluna 1.
tests:
- not_null
- name: coluna 2
description: descrição da coluna 2.

OBS: e esse arquivo também pode ser usado nas pastas data e macro, alterando sequencialmente o nome ‘models:’ por ‘seeds:’ e ‘macros:’.

>Staging: São tabelas intermediárias normalmente usadas para corte temporal para auxiliar na performance de cargas incrementais, como também para regras gerais que serão usadas apenas como auxiliares de outras modelagens principais.

Tests

Testes são afirmações que você faz sobre seus modelos e outros recursos em seu projeto dbt (por exemplo, sources e seeds). Você pode usar os testes para melhorar a integridade do SQL em cada modelo fazendo afirmações sobre os resultados gerados. Ou também testar se uma coluna especificada em um modelo contém apenas valores não nulos, valores exclusivos ou valores que possuem um valor correspondente em outro modelo.

Ao executar o comando dbt test, o dbt informará se cada teste em seu projeto foi aprovado ou reprovado e é uma função muito importante para garantir a qualidade dos dados armazenados. Existem dois tipos de teste, o genérico presente no schema.yml dos modelos e os “singular tests”. Nesse segundo tipo eles ficam armazenados nessa pasta tests em formato de .sql.

Exemplo:

{{
config(
tags=["data_test"],
severity='error'
)
}}

WITH dms_sync_diff AS(

SELECT
max(TO_DATE(day, 'YYYY-MM-DD')) AS synced_at,
GETDATE() AS ts_now,
DATEDIFF(min, synced_at, ts_now) AS min_diff,
CASE
WHEN DATE_PART(dow, GETDATE()) IN (0, 6)
THEN 1440 ELSE 4320
END AS minutes_allowed
FROM {{ source('source_name', 'table_name') }}

)

SELECT *
FROM dms_sync_diff
WHERE min_diff > minutes_allowed

Project

Todo projeto DBT precisa de um arquivo dbt_project.yml. Assim que o dbt sabe que um diretório é um projeto dbt. Ele também contém informações importante que dizem ao dbt como operar o projeto.

Explicando o arquivo em partes:

>Parte 1: Na parte inicial identificamos:

  • Nome, versão do projeto, versão requerida do dbt e nome do profile, que deverá ser o mesmo constado no arquivo profiles.yml exemplificado na primeira parte desse guia.
name: "dbt_pebmed"
version: "1.0.0"
config-version: 2
require-dbt-version: [">=1.6.0", "<1.7.0"]

profile: "pebmed"
  • Nome das pastas criadas de acordo com as funções existentes no DBT e as pastas que gostaríamos de deixar ocultas.
model-paths: ["models"]
test-paths: ["tests"]
seed-paths: ["data"]
macro-paths: ["macros"]

target-path: "target"

clean-targets:
- "target"
- "logs"
- "dbt_packages"
  • É possível também definir alguns comandos para rodarem no fim de toda execução do projeto. No nosso caso chamamos as macros criadas para dar permissão a grupos do Redshift em todo novo schema criado no DBT.
on-run-end:
- "{{ grant_select_on_schemas(schemas, 'group_1) }}"
- "{{ grant_all_on_schemas(schemas, 'group_2') }}"

>Parte 2: No meio do arquivo identificamos as pastas que contém as modelagens/seeds para marcar qual sufixo queremos que seja adicionado nos schemas do Redshift (exemplo: dbt_prod_source). Se a pasta ainda não possui nenhum modelo criado, o ideal é deixar comentado para não gerar um alerta sempre que um modelo for executado.

A tag ‘+persist_docs:’ faz com que os schema.yml das modelagens gerem o comment da tabela no data warehouse.

models:
elementary:
+schema: elementary

dbt_pebmed:
+persist_docs:
relation: true
columns: true

staging:
+schema: staging

sources:
+schema: source

marts:
+schema: marts

seeds:
dbt_pebmed:
+persist_docs:
relation: true
columns: true
+enabled: true
+schema: seed_data

>Parte 3: No final do código identificamos as variáveis a serem usadas nas modelagens para quando precisarmos fazer uma alteração, ela já impactar todos os modelos ao mesmo tempo.

vars:
'dbt_date:time_zone': 'America/Sao_Paulo'
testing_days: 1
filter_days_raw: 2
filter_days: 2

Arquivos adicionais

Vou agora abordar alguns arquivos que não são essenciais, mas têm sido úteis em nossa prática diária com o projeto.

  • .gitignore: Neste arquivo, especificamos quais pastas e arquivos devem ser ignorados pelo Git. Isso significa que sempre que fizermos um novo commit, os arquivos listados aqui não serão incluídos. A seguir, estão as pastas que atualmente excluímos, já que contêm dados de pacotes ou execuções individuais.
target/    
dbt_packages/
logs/
  • README.md: É um arquivo frequentemente utilizado em projetos Git, onde você pode detalhar minuciosamente o propósito e as especificações do seu projeto. Isso significa que sempre que alguém abrir o repositório, o conteúdo deste arquivo estará em destaque.
    A estrutura desse arquivo segue padrões de linguagem específicos. Como exemplo, deixo o README criado no repositório do DBT:
    - Repositório: [link]
    - README original: [link]
  • .sqlfluff: É uma ferramenta de formatação automática de código SQL que desempenhou um papel fundamental na padronização dos nossos modelos. Para obter mais detalhes sobre essa funcionalidade, recomendo a leitura do artigo: Automatizando a formatação de códigos SQL com SQLFluff.
  • selectors.yml: À medida que a necessidade de personalizar novos fluxos de execução surge, este arquivo ganha importância. Com ele, é possível organizar o plano de execução dos Jobs, tudo de forma documentada e controlada por versão.
    Por padrão, nosso principal job executa a sequência das sources diariamente, mas também conseguimos personalizar alguns jobs para situações específicas. Para realizar a execução, utilizamos o comando: dbt run --selector job_models_daily
    Abaixo, apresento um exemplo de configuração do arquivo, no qual podemos definir os Jobs, suas descrições e quais modelos devem ser incluídos ou excluídos. Além disso, há a opção de determinar se, ao executar, os modelos pais e filhos da pasta ou arquivo selecionado também serão acionados:
selectors:
- name: job_models_daily
description: Job que executa os modelos do DBT diariamente, exceto os modelos X.
definition:
union:
- method: path
value: models/sources
children: true
- exclude:
- method: path
value: models/sources/folder_2
children: true

- name: job_models_2
description: Job que executa o modelo X e seus pais.
definition:
union:
- method: path
value: models/marts/folder_2/model_name.sql
children: false
parents: true

Na primeira parte deste guia, dei uma visão geral da ferramenta e expliquei como configurá-la em “Desvendando a arquitetura e configuração inicial”.

Agora, nesta segunda parte, mostrei as pastas e arquivos principais para o desenvolvimento do seu projeto DBT. Depois de organizar tudo, é só fazer o commit dos arquivos no repositório que você preferir, para compartilhar com o resto do time e manter um bom controle de versão. E para o próximo artigo, vou apresentar o DBT Cloud, que é o orquestrador que estamos usando no nosso projeto atualmente para você conseguir agendar suas primeiras modelagens.

Espero que este guia seja útil para aqueles que têm interesse em uma solução governada de transformação de dados ou para aqueles que desejam aprimorar seus conhecimentos nesta ferramenta. O DBT é uma ferramenta incrível que transformou a interação diária entre nossa equipe de dados e trouxe maior governança aos conjuntos de dados criados. Se tiver alguma dúvida ou sugestão, não hesite em entrar em contato comigo pelo Linkedin.

English version: https://medium.com/@alice_thomaz/54bd5f3c8100

Parte 3: Orquestrando o projeto.

--

--