ServicesÀ proposNotesContact Me contacter →
EN FR
Note

Patterns de la couche base dbt

Ce qui appartient aux modèles base dbt — renommage, transtypage, déduplication, unnesting — et la seule exception à la règle du pas de jointures.

Planté
dbtdata modelingdata engineering

Les modèles base rendent les données sources brutes utilisables sans ajouter d’interprétation. Un modèle base devrait être lisible par quiconque est non familier avec le domaine métier — s’il nécessite des explications, il en fait trop.

L’architecture trois couches dbt place la couche base au bas de la lignée : source → base → intermédiaire → marts. Les modèles base agissent comme un contrat entre le projet dbt et les données brutes. Quand un schéma source change, seuls les modèles base se brisent — pas tout ce qui est en aval. Quand il faut déboguer un calcul, le tracer à travers l’intermédiaire vers une couche base fiable est simple.

Ce qui appartient aux modèles base

Renommer les colonnes selon les standards de nommage. Les systèmes sources utilisent des noms incohérents. Le projet devrait avoir une seule convention. Si le pattern double underscore est utilisé, user_id devient customer__id, created_at devient order__created_at. C’est du travail mécanique, pas de l’interprétation.

Transtyper les types de données. Chaînes vers timestamps, entiers vers booléens, varchars vers des types numériques appropriés. Les systèmes sources sont souvent négligents avec les types. Corriger cela ici pour que les modèles en aval puissent faire confiance aux types de données qu’ils reçoivent.

Dédupliquer les enregistrements sources. Beaucoup d’outils ETL produisent des lignes dupliquées — replays CDC, chargements incrémentaux se chevauchant, logique de réessai. Utiliser ROW_NUMBER() avec QUALIFY (sur BigQuery) pour ne garder que la version la plus récente de chaque enregistrement. C’est un pattern de fonction fenêtre utilisé constamment.

Unnester les champs imbriqués ou répétés. Courant avec les données d’événements GA4, les payloads JSON, et les réponses API qui stockent des tableaux dans les lignes. Aplatir ces données en base pour que les modèles en aval travaillent avec des données tabulaires standard.

Filtrer les enregistrements invalides. Données de test, lignes soft-deleted, enregistrements qui n’auraient jamais dû être chargés. Les supprimer ici pour qu’aucun modèle en aval n’ait à se souvenir de les filtrer.

Ce qui n’appartient pas à la couche base

Les jointures. Les modèles base ont une relation 1-à-1 avec une table source. Une source, un modèle base. Les jointures sont des préoccupations intermédiaires.

La logique métier ou les calculs. Pas d’instructions CASE WHEN catégorisant les commandes. Pas de calculs de marge. Pas de niveaux client. Si les données sont interprétées, c’est la mauvaise couche. Déplacer vers les modèles intermédiaires.

Les agrégations de toute sorte. Les modèles base n’utilisent jamais GROUP BY comme sortie finale. Chaque ligne de la source devrait produire au plus une ligne dans le modèle base (moins si on filtre ou déduplique).

La seule exception à la règle du pas de jointures

Certains outils ETL divisent ce qui est logiquement un seul endpoint API en plusieurs tables. Si la source a orders et order_metadata qui auraient toujours dû être une seule table, les joindre en base est acceptable. On corrige un artefact ETL, pas on ajoute de la logique métier.

Le test : l’API du système source retournerait-elle ces éléments comme un seul objet ? Si oui, les fusionner en base est acceptable. Si non — si on joint des commandes à des clients, par exemple — cela appartient à l’intermédiaire.

Un modèle base typique

Cet exemple montre tous les patterns standard : sélectionner depuis une source, dédupliquer, renommer, transtyper, et filtrer.

{{ config(
materialized='table',
tags=['base', 'shopify']
) }}
WITH source AS (
SELECT
id,
user_id,
created_at,
total,
status,
is_deleted,
_loaded_at
FROM {{ source('shopify', 'orders') }}
),
deduplicated AS (
SELECT
id,
user_id,
created_at,
total,
status,
is_deleted,
_loaded_at
FROM source
QUALIFY ROW_NUMBER() OVER (
PARTITION BY id
ORDER BY _loaded_at DESC
) = 1
),
renamed AS (
SELECT
id AS order__id,
user_id AS customer__id,
created_at AS order__created_at,
CAST(total AS FLOAT64) AS order__amount_usd,
status AS order__status,
CAST(is_deleted AS BOOL) AS order__is_deleted,
_loaded_at
FROM deduplicated
)
SELECT
order__id,
customer__id,
order__created_at,
order__amount_usd,
order__status,
order__is_deleted,
_loaded_at
FROM renamed
WHERE order__is_deleted = FALSE

Notez la structure CTE : source → deduplicated → renamed → SELECT final. C’est un pattern, pas un mandat, mais il rend chaque modèle base lisible et cohérent à travers le projet. La clause WHERE finale filtre les enregistrements soft-deleted — ils ne devraient jamais se retrouver en aval.

Il n’y a aucune logique métier ici. Pas de calculs de marge, pas de catégorisation des commandes, pas de segmentation des clients. Juste des données propres, nommées de manière cohérente, correctement typées.

Conventions CTE des modèles base

Une structure CTE cohérente à travers tous les modèles base rend le projet navigable :

  1. source — Sélectionner depuis la macro {{ source() }}. Lister les colonnes explicitement plutôt qu’utiliser SELECT *, pour que les changements de schéma n’ajoutent pas silencieusement des colonnes non souhaitées.
  2. deduplicated — Supprimer les enregistrements dupliqués en utilisant ROW_NUMBER() partitionné par la clé primaire, ordonné par un timestamp de métadonnées comme _loaded_at.
  3. renamed — Renommer les colonnes selon les conventions du projet et transtyper les types de données.
  4. SELECT final — Appliquer les filtres (soft deletes, données de test) et sélectionner la liste finale de colonnes.

Tous les modèles base n’ont pas besoin des quatre étapes. Une source sans doublons saute le CTE de déduplication. Une source avec des noms de colonnes propres peut combiner le renommage dans le CTE source. Mais quand les étapes sont présentes, elles devraient suivre cet ordre.

Matérialisation

Les modèles base devraient presque toujours être matérialisés comme table. L’architecture trois couches dbt bénéficie que les modèles base soient interrogeables pour le débogage — si on matérialise comme view, chaque requête contre la base recalcule. Si on matérialise comme ephemeral, le modèle n’existe pas du tout dans l’entrepôt, rendant le débogage impossible.

L’exception : les sources à très fort volume (événements GA4, données de clickstream) où les modèles base gèrent des milliards de lignes. Ces sources bénéficient d’une matérialisation incrémentale pour éviter de reconstruire la table entière à chaque run. Configurer cela au niveau du dossier dans dbt_project.yml plutôt que par modèle.

Indicateur de complexité

Si un modèle base a besoin de commentaires inline pour expliquer ce qu’il fait, il est probablement trop complexe. Les modèles base devraient être suffisamment mécaniques pour que tout développeur puisse lire le SQL et comprendre la transformation sans explication. La logique métier — tout ce qui catégorise, note ou interprète les données — appartient à l’intermédiaire, pas ici.