ServicesÀ proposNotesContact Me contacter →
EN FR
Note

Patterns d'implémentation dbt-expectations sur BigQuery

Implémentation réelle de dbt-expectations sur BigQuery — YAML complet pour GA4 et les données publicitaires, placement des tests par couche DAG, et une checklist de démarrage pratique.

Planté
dbtbigqueryga4data qualitytesting

La référence des tests catalogue ce que fait chaque test. Cette note montre comment appliquer ces tests à un projet BigQuery d’analytics réel. Les exemples utilisent les données d’événements GA4 et les modèles de performance publicitaire — deux des sources de données les plus courantes en marketing analytics — mais les patterns se généralisent à n’importe quel projet BigQuery.

Modèle base des événements GA4

Les données GA4 ont des particularités connues : des délais de traitement de 24 à 48 heures, des noms d’événements qui doivent suivre une convention snake_case, un user_pseudo_id avec un format numérique spécifique, et des timestamps qui ne doivent jamais être dans le futur. Une suite de tests pour un modèle base doit encoder tout cela :

version: 2
models:
- name: base__ga4__events
description: "Événements GA4 de base avec un nettoyage basique appliqué"
tests:
# La table doit avoir des données récentes (en tenant compte du délai de 24-48h de GA4)
- dbt_expectations.expect_row_values_to_have_recent_data:
datepart: hour
interval: 48
# Pas de jours manquants dans la série temporelle
- dbt_expectations.expect_row_values_to_have_data_for_every_n_datepart:
date_col: event_date
date_part: day
test_start_date: "date_sub(current_date(), interval 90 day)"
test_end_date: "date_sub(current_date(), interval 2 day)"
columns:
- name: event_id
description: "Identifiant unique de l'événement"
tests:
- unique
- not_null
- name: event_timestamp
description: "Horodatage de l'événement en UTC"
tests:
- not_null
- dbt_expectations.expect_column_values_to_be_between:
min_value: "'2020-01-01 00:00:00'"
max_value: "current_timestamp()"
- name: event_name
description: "Nom de l'événement GA4"
tests:
- not_null
- dbt_expectations.expect_column_values_to_match_regex:
regex: '^[a-z_]+$'

Quelques éléments à noter :

L’intervalle de fraîcheur est de 48 heures, pas 24. L’export GA4 vers BigQuery a un délai bien documenté. Fixer cela à 24 heures produirait de faux positifs à chaque fois que l’export s’exécute en retard, ce qui érode la confiance dans la suite de tests. Faire correspondre l’intervalle au SLA réel de la source, pas à un idéal.

Les bornes du test de complétude de la série temporelle sont compatibles avec les partitions. date_sub(current_date(), interval 90 day) comme début et date_sub(current_date(), interval 2 day) comme fin. La fenêtre de 90 jours évite de scanner tout l’historique de la table (qui peut représenter des années d’événements). La marge de 2 jours à la fin évite les échecs parce que les données d’aujourd’hui et d’hier n’ont pas fini de traiter. Sur BigQuery, ces expressions de date permettent le partition pruning, maintenant les coûts bas.

Le regex event_name est strict. Les noms d’événements GA4 doivent être en snake_case minuscule. Si vous voyez Page_View ou purchase-complete en production, quelque chose est mal configuré dans la propriété GA4 ou un événement personnalisé a été mal nommé. Capturer cela à la couche base empêche les modèles en aval de supprimer silencieusement des événements qu’ils ne reconnaissent pas.

Mart de performance publicitaire

Un mart de performance publicitaire agrège les données entre plateformes (Google Ads, Meta Ads, etc.) dans un grain quotidien unifié. La suite de tests valide la clé composite, la récence, les plages de valeurs, et la cohérence des règles métier :

version: 2
models:
- name: mrt__marketing__ads_performance
description: "Performance publicitaire quotidienne par campagne"
tests:
# Clé primaire composite
- dbt_expectations.expect_compound_columns_to_be_unique:
column_list: ["date", "platform", "campaign_id"]
# Doit avoir des données récentes
- dbt_expectations.expect_row_values_to_have_recent_data:
datepart: day
interval: 2
columns:
- name: date
tests:
- not_null
- dbt_expectations.expect_column_values_to_be_between:
min_value: "'2023-01-01'"
max_value: "current_date()"
- name: platform
tests:
- not_null
- accepted_values:
values: ['google_ads', 'meta_ads', 'tiktok_ads', 'linkedin_ads']
- name: campaign_id
tests:
- not_null
- name: impressions
tests:
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 1000000000
- name: clicks
tests:
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 100000000
- name: spend
tests:
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 10000000
# Avertir si la dépense quotidienne moyenne semble incorrecte
- dbt_expectations.expect_column_mean_to_be_between:
min_value: 10
max_value: 100000
row_condition: "date >= date_sub(current_date(), interval 30 day)"
config:
severity: warn
- name: conversions
tests:
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 1000000
- name: roas
description: "Retour sur les dépenses publicitaires"
tests:
# Le ROAS doit être entre 0 et 100 (là où il est calculable)
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 100
row_condition: "spend > 0"
config:
severity: warn

Patterns clés dans cet exemple :

La clé composite utilise expect_compound_columns_to_be_unique plutôt qu’un test unique sur une seule colonne. Le grain de la table est une ligne par combinaison date-plateforme-campagne. Le test unique natif de dbt ne peut pas exprimer cela. Sans validation de la clé composite, les lignes dupliquées gonflent silencieusement les métriques — une campagne semble avoir le double des dépenses, le double des impressions, et soudainement vos calculs de ROAS n’ont plus de sens.

La colonne spend a à la fois une vérification de plage au niveau des lignes et une vérification statistique. La vérification de plage (0 à 10 000 000) capture les valeurs individuelles manifestement invalides. La vérification de la moyenne (10 à 100 000) capture les glissements de distribution où les valeurs individuelles sont correctes mais le pattern global est faux. Une source qui envoie soudainement toutes des lignes avec une dépense nulle passerait la vérification de plage mais échouerait à la vérification de la moyenne.

La vérification de la moyenne utilise row_condition pour le filtrage des partitions. date >= date_sub(current_date(), interval 30 day) limite le calcul statistique aux 30 derniers jours. Sans cela, BigQuery scanne la table entière pour calculer la moyenne — coûteux et de plus en plus hors de propos à mesure que vous accumulez des données historiques. Le filtre de partition maintient les coûts bas et rend la statistique représentative du comportement récent plutôt que des moyennes de tous les temps.

La validation du ROAS utilise row_condition: "spend > 0" pour éviter de diviser par zéro ou de tester des lignes où le ROAS est indéfini. La sévérité warn reconnaît que les valeurs aberrantes du ROAS pourraient être réelles (une campagne virale avec des dépenses minimales peut avoir un ROAS légitime de 50x) — elles nécessitent une investigation, pas des arrêts de pipeline.

Placement des tests par couche DAG

L’endroit où vous placez les tests compte autant que les tests eux-mêmes. L’architecture en trois couches suggère différentes priorités de test à chaque couche.

Couche base : capturer les problèmes à la source

Se concentrer sur la fraîcheur, la validation du schéma et les vérifications de format. Les problèmes capturés ici empêchent les défaillances en cascade dans l’ensemble du DAG :

models:
- name: base__ga4__events
tests:
- dbt_expectations.expect_row_values_to_have_recent_data:
datepart: hour
interval: 48
columns:
- name: user_pseudo_id
tests:
- dbt_expectations.expect_column_values_to_match_regex:
regex: '^[0-9]+\.[0-9]+$'

Les modèles base sont votre première ligne de défense. Un changement de format dans user_pseudo_id capturé ici produit un signal clair et immédiat. Le même problème capturé à la couche mart, après les jointures et les agrégations, produit une cascade confuse d’échecs en aval.

Couche intermédiaire : valider l’intégrité des jointures

Les modèles intermédiaires joignent et remodèlent principalement les données. Les tests critiques sont les comparaisons du nombre de lignes (les jointures ne doivent pas supprimer silencieusement des lignes) et l’intégrité référentielle :

models:
- name: int__orders__enriched
tests:
- dbt_expectations.expect_table_row_count_to_equal_other_table:
compare_model: ref('base__shopify__orders')

Si la base a 50 000 commandes et l’intermédiaire en a 49 000 après enrichissement, une condition de jointure supprime des lignes. Ce test fait apparaître le problème avant que les commandes manquantes ne se propagent vers les marts et les tableaux de bord.

Couche mart : appliquer les règles métier

Les marts sont les tables que les parties prenantes interrogent. Les tests ici protègent les sorties finales avec la validation des règles métier et les vérifications de cohérence des agrégations :

models:
- name: mrt__marketing__campaign_performance
columns:
- name: roas
tests:
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 100
row_condition: "spend > 0"

Le principe général : l’intensité des tests doit augmenter vers les extrémités du DAG. Les sources sont l’endroit où les problèmes entrent. Les marts sont l’endroit où ils atteignent les consommateurs. Les couches intermédiaires reçoivent une couverture plus légère axée sur l’intégrité des jointures.

Point de départ : trois tests sur un modèle

Après la configuration de dbt-expectations, un point de départ pratique est trois tests sur le modèle le plus critique :

1. Fraîcheur. expect_row_values_to_have_recent_data sur la colonne de timestamp principale — capture les données périmées.

columns:
- name: updated_at
tests:
- dbt_expectations.expect_row_values_to_have_recent_data:
datepart: hour
interval: 24

2. Format. expect_column_values_to_match_regex sur une colonne d’identifiant clé — capture les changements de format en amont.

columns:
- name: customer_email
tests:
- dbt_expectations.expect_column_values_to_match_regex:
regex: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
row_condition: "customer_email is not null"

3. Plage. expect_column_values_to_be_between sur une colonne de KPI numérique — capture les valeurs impossibles.

columns:
- name: revenue
tests:
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 10000000

Étendre à partir de ces tests en fonction de ce qui casse en production — les incidents doivent piloter la couverture des tests plutôt que les modes de défaillance anticipés.

Élargir la couverture

Une fois vos trois premiers tests en cours d’exécution, utiliser cet ordre de priorité pour étendre :

  1. Clés primaires sur chaque modèleunique + not_null (ou expect_compound_columns_to_be_unique pour les clés composites). Ces tests sont non négociables.
  2. Fraîcheur sur chaque modèle adjacent à une sourceexpect_row_values_to_have_recent_data sur les modèles base alimentés par des sources externes.
  3. Vérifications de plage sur chaque colonne de KPIexpect_column_values_to_be_between sur les métriques qui alimentent les tableaux de bord.
  4. Validation de format sur les identifiantsexpect_column_values_to_match_regex sur les colonnes avec des formats connus (emails, SKUs, IDs).
  5. Vérifications statistiques sur les agrégats critiquesexpect_column_mean_to_be_between avec severity: warn sur les métriques de grande valeur.
  6. Complétude de la série temporelleexpect_row_values_to_have_data_for_every_n_datepart sur les modèles où les jours manquants passeraient inaperçus.

Chaque incident en production doit aboutir à un nouveau test qui empêche la récurrence.