Fastodata / Onboarding

Module d'onboarding — dbt (data build tool)

Contenu pédagogique produit par Bill (QA). Destiné à être intégré dans onboarding.fastodata.com.
Public : ingénieur data avec SQL solide. Format : théorie d'abord → exercices → quiz corrigé.
Stack de référence de l'équipe : dbt + BigQuery (+ orchestration Kestra/Airflow).
Durée estimée : ~3-4 h de lecture active + exercices.


Objectifs pédagogiques

À la fin de ce module, l'apprenant sait :

  1. Expliquer le rôle de dbt (le « T » de l'ELT) et ce qu'il ne fait pas.
  2. Construire un lignage avec ref() / source() et lire un DAG.
  3. Choisir la bonne matérialisation (view / table / incremental / ephemeral).
  4. Écrire tests et documentation en YAML.
  5. Utiliser Jinja & macros, et débugger avec dbt compile.
  6. Comprendre snapshots (SCD2), sources/freshness, packages.

Module 1 — Ce qu'est dbt (et ce qu'il n'est pas)

dbt compile du SQL et l'exécute dans le data warehouse (BigQuery ici). Il ne stocke ni ne calcule rien lui-même : l'entrepôt fait le travail, dbt orchestre.

Apports au-dessus du SQL brut :

⚠️ Piège mental : un modèle dbt n'est pas un script CREATE TABLE. On écrit uniquement le SELECT ; dbt génère le CREATE … AS selon la matérialisation.


Module 2 — ref(), source() et le DAG

dbt lit tous les ref()/source(), déduit le DAG, et ordonne l'exécution automatiquement.

Sélection par graphe :

Architecture en couches (standard équipe) : staging (stg_) → intermediate (int_) → publish/marts.


Module 3 — Les matérialisations

Type Génère Quand
view CREATE VIEW staging, transfo légère
table CREATE TABLE AS (full refresh) marts lus souvent, calcul lourd
incremental insère/merge seulement le nouveau grosses tables append-only
ephemeral pas d'objet ; inliné en CTE chez l'aval logique intermédiaire jamais requêtée seule

Incremental (clé sur BigQuery) :

{{ config(materialized='incremental', unique_key='order_id') }}
select * from {{ source('raw','orders') }}
{% if is_incremental() %}
  where updated_at > (select max(updated_at) from {{ this }})
{% endif %}

Module 4 — Tests & documentation (YAML)

models:
  - name: stg_sales
    description: "Ventes nettoyées, une ligne par transaction"
    columns:
      - name: sale_id
        tests: [unique, not_null]
      - name: customer_id
        tests:
          - not_null
          - relationships: { to: ref('stg_customers'), field: customer_id }
      - name: status
        tests:
          - accepted_values: { values: ['paid','refunded','pending'] }

4 tests génériques natifs : unique, not_null, relationships, accepted_values.
Au-delà → package dbt-utils ou tests singuliers (.sql dans tests/ qui doit renvoyer 0 ligne).

dbt build = run + test dans l'ordre du DAG (le réflexe vs run seul).


Module 5 — Jinja & macros

{% for s in ['paid','refunded','pending'] %}
sum(case when status = '{{ s }}' then amount end) as amount_{{ s }}{{ "," if not loop.last }}
{% endfor %}
{% macro cents_to_euros(column) %}round({{ column }} / 100.0, 2){% endmacro %}

Réflexe debug n°1 : dbt compile → SQL final dans target/compiled/. Toujours le 1er endroit à regarder.
Piège : Jinja s'exécute à la compilation, jamais au runtime. Il ne voit pas les données. Pour boucler sur des valeurs en base → run_query()/statement (introspection).


Module 6 — Snapshots (SCD type 2)

Historiser les changements d'un enregistrement dans le temps (ex. une commande qui change de statut).

{% snapshot orders_snapshot %}
{{ config(target_schema='snapshots', unique_key='order_id',
          strategy='timestamp', updated_at='updated_at') }}
select * from {{ source('raw','orders') }}
{% endsnapshot %}

Module 7 — Sources, freshness & packages

sources:
  - name: raw
    tables:
      - name: orders
        loaded_at_field: _loaded_at
        freshness:
          warn_after:  { count: 12, period: hour }
          error_after: { count: 24, period: hour }

dbt source freshness → alerte si la donnée brute est trop vieille.


Exercices (fil rouge : un mini-pipeline ventes)

  1. Staging : écris stg_orders à partir de source('raw','orders') — caste amount_cents en int, renomme tsorder_at. Matérialisation ? Justifie.
  2. Tests : ajoute le YAML — order_id unique+not_null, status ∈ {paid,refunded,pending}, customer_id en relation avec stg_customers.
  3. Incremental : transforme fct_orders (10 M lignes, append-only) en incremental sur order_id filtré par order_at. Écris le bloc is_incremental().
  4. Macro : crée cents_to_euros() et utilise-la dans fct_orders.
  5. Sélection : quelle commande pour rejouer fct_orders ET tout ce qui en dépend, sans toucher l'amont ?

Quiz final corrigé

Q1. Pourquoi {{ ref('x') }} plutôt qu'un nom de table en dur ?
→ Construit le DAG/ordre auto ; environment-aware (dev/prod) ; refactor sûr.

Q2. Table d'événements 2 Md append-only : matérialisation + rôle de is_incremental() ?
incremental (sinon rebuild complet = lent/cher). is_incremental() n'est vrai que si la table existe et hors --full-refresh ; il borne le WHERE aux nouvelles lignes tout en autorisant le rebuild complet.

Q3. relationships vs accepted_values ?
relationships = intégrité référentielle (valeur existe dans une autre table). accepted_values = appartenance à une liste figée (contrainte de domaine).

Q4. dbt run --select +int_orders exécute quoi ?
int_orders et tout son amont (le + devant = ancêtres). Pas l'aval.

Q5. ephemeral : à quoi ça sert, qu'est-ce qui est créé en base ?
→ Factoriser une logique intermédiaire jamais requêtée seule ; aucun objet créé, dbt l'inline en CTE chez l'aval.

Q6. {% for %} s'exécute avant ou pendant la requête BigQuery ? Conséquence ?
→ Avant (compilation Jinja). Conséquence : ne peut pas dépendre d'une valeur de ligne ; pour du dynamique-données → run_query().

Q7. Modèle au comportement bizarre, plein de Jinja. Première commande ?
dbt compile, puis lire target/compiled/… pour voir le SQL réel envoyé.


Pour aller plus loin

→ BigQuery → Git & CI → Airflow → Kestra