Adrienne Vermorel

Stratégie microbatch incrémental dans dbt 1.9 : guide pratique

Si vous avez déjà écrit des modèles incrémentaux dans dbt, vous connaissez la danse du is_incremental(). Vous écrivez la logique de filtrage, gérez les cas limites pour les données en retard, et implémentez manuellement les procédures de backfill. Ça fonctionne, mais c’est fastidieux.

dbt 1.9 a introduit microbatch, une stratégie incrémentale qui gère le traitement partitionné par le temps différemment. Au lieu d’une seule requête pour toutes les nouvelles données, microbatch exécute des requêtes séparées pour chaque période (heure, jour, mois). Ça change la façon dont on pense les modèles incrémentaux.

Ce guide passe en revue quand microbatch a du sens, comment le configurer, et les points d’attention selon les data warehouses.

Comment microbatch se distingue de l’incrémental traditionnel

Les modèles incrémentaux traditionnels exécutent une seule requête qui traite toutes les nouvelles données depuis la dernière exécution. Vous écrivez le bloc is_incremental(), définissez votre fenêtre de lookback, et espérez que rien ne passe entre les mailles.

Microbatch inverse cette logique. Au lieu d’une seule requête, dbt exécute des requêtes séparées pour chaque batch temporel :

AspectIncrémental traditionnelMicrobatch
Structure de requêteUn seul SQL pour toutes les nouvelles donnéesUn SQL par batch
Responsabilité utilisateurÉcrire la logique is_incremental()Aucune logique conditionnelle
Définition du batchDéfinie dans le SQL par l’utilisateurConfigurée via event_time, batch_size
Granularité de retryModèle entierBatches individuels
BackfillLogique custom nécessaireFlags intégrés

Si le traitement échoue à mi-chemin d’un mois de données, l’incrémental traditionnel relance tout. Microbatch ne relance que les batches en échec.

Configuration de base

Un modèle microbatch nécessite trois configurations : la colonne de timestamp (event_time), la granularité du batch (batch_size), et la date la plus ancienne à traiter (begin).

{{ config(
materialized='incremental',
incremental_strategy='microbatch',
event_time='session__started_at',
batch_size='day',
begin='2020-01-01'
) }}
SELECT
session_id,
user_id,
session__started_at,
session__page_views,
session__duration_seconds
FROM {{ ref('base__app__sessions') }}

Pas de bloc is_incremental(). dbt gère le filtrage automatiquement à partir de votre colonne event_time.

Les options de batch_size sont hour, day, month ou year. Choisissez en fonction du volume de données et de la fréquence de mise à jour souhaitée. Le batch quotidien est le choix le plus courant pour les données événementielles.

Gérer les données en retard

Les données en retard sont un problème constant avec les modèles incrémentaux. Des enregistrements arrivent après que leur période a déjà été traitée, ce qui crée des trous dans vos données.

Microbatch gère ce cas avec le paramètre lookback :

{{ config(
materialized='incremental',
incremental_strategy='microbatch',
event_time='event_occurred_at',
batch_size='day',
lookback=3,
begin='2020-01-01'
) }}
SELECT
event_id,
user_id,
event_occurred_at,
event_name,
event_properties
FROM {{ ref('base__app__events') }}

Avec lookback=3, chaque exécution retraite les trois batches précédents en plus des nouveaux. Si on est le 10 janvier et que vous lancez le modèle, dbt traite les batches du 7, 8, 9 et 10 janvier.

Ce paramètre remplace le pattern de lookback manuel qu’on écrirait dans un modèle incrémental traditionnel :

-- L'ancienne méthode
{% if is_incremental() %}
WHERE event_date >= (
SELECT dateadd(day, -3, max(event_date))
FROM {{ this }}
)
{% endif %}

Comportement spécifique par warehouse

Microbatch utilise des stratégies différentes selon votre warehouse. C’est important parce que cela affecte la configuration supplémentaire dont vous pourriez avoir besoin.

WarehouseStratégie utiliséeConfiguration supplémentaire
BigQueryinsert_overwritepartition_by requis
Snowflakedelete+insertAucune
Databricksreplace_whereAucune
Redshiftdelete+insertAucune
PostgreSQLmergeunique_key requis

Configuration BigQuery

Le microbatch sur BigQuery nécessite une configuration de partition alignée avec votre event_time :

{{ config(
materialized='incremental',
incremental_strategy='microbatch',
partition_by={
"field": "session__started_at",
"data_type": "timestamp",
"granularity": "day"
},
event_time='session__started_at',
batch_size='day',
begin='2020-01-01'
) }}

La granularité du partition_by devrait correspondre à votre batch_size. Un désalignement ne cassera pas votre modèle, mais réduira l’efficacité.

Configuration PostgreSQL

PostgreSQL utilise merge en interne, il faut donc spécifier une unique_key :

{{ config(
materialized='incremental',
incremental_strategy='microbatch',
unique_key='session_id',
event_time='session__started_at',
batch_size='day',
begin='2020-01-01'
) }}

Filtrage automatique des modèles upstream

Une fonctionnalité utile de microbatch est le filtrage automatique des modèles en amont. Si un modèle que vous référencez avec ref() a aussi un event_time configuré, dbt le filtre automatiquement pour correspondre au batch en cours.

-- Si base__app__page_views a un event_time configuré,
-- dbt le filtre automatiquement pour chaque batch
SELECT
page_view_id,
session_id,
user_id,
page_url,
viewed_at
FROM {{ ref('base__app__page_views') }}

Cela évite les full table scans sur vos tables sources. Sans ce mécanisme, chaque batch lirait la table upstream en entier avant de filtrer.

Pour les tables sans event_time (comme les tables de dimensions), dbt lit la table complète à chaque batch. Ce n’est généralement pas un problème pour les petites tables de référence, mais gardez-le en tête pour les tables plus volumineuses.

Pour désactiver le filtrage automatique sur une référence spécifique :

-- Forcer la lecture complète même si event_time est configuré
SELECT
product_id,
product_name,
product_category
FROM {{ ref('mrt__product__products').render() }}

Commandes de backfill

Le support intégré du backfill élimine beaucoup de travail manuel. Au lieu d’écrire des scripts custom ou de surcharger des variables, vous utilisez des flags CLI :

Terminal window
# Traiter une plage de dates spécifique
dbt run --select int__sessions_aggregated --event-time-start "2024-09-01" --event-time-end "2024-09-04"
# Relancer uniquement les batches en échec du dernier run
dbt retry
# Full refresh avec historique borné
dbt run --full-refresh --event-time-start "2024-01-01" --event-time-end "2024-02-01"

Les flags --event-time-start et --event-time-end fonctionnent aussi bien avec les runs normaux qu’avec les full refreshes. Ça permet de retraiter des périodes spécifiques sans toucher au reste des données.

Se protéger contre les full refreshes accidentels

Reconstruire de grosses tables incrémentales peut coûter cher. Microbatch permet d’empêcher les full refreshes accidentels :

{{ config(
materialized='incremental',
incremental_strategy='microbatch',
event_time='event_occurred_at',
batch_size='day',
begin='2020-01-01',
full_refresh=false
) }}

Avec full_refresh=false, un dbt run --full-refresh sur ce modèle échouera au lieu de tout reconstruire. Vous pouvez toujours faire des refreshes bornés avec les flags --event-time-start et --event-time-end.

Migration depuis l’incrémental traditionnel

Convertir un modèle incrémental existant vers microbatch est généralement simple.

Version incrémentale traditionnelle :

{{ config(
materialized='incremental',
incremental_strategy='delete+insert',
unique_key='date_day'
) }}
SELECT
event_id,
user_id,
event_occurred_at,
event_name,
event_properties
FROM {{ ref('base__app__events') }}
{% if is_incremental() %}
WHERE date_day >= (
SELECT {{ dbt.dateadd("day", -3, "max(date_day)") }}
FROM {{ this }}
)
{% endif %}

Version microbatch :

{{ config(
materialized='incremental',
incremental_strategy='microbatch',
event_time='event_occurred_at',
batch_size='day',
lookback=3,
begin='2020-01-01'
) }}
SELECT
event_id,
user_id,
event_occurred_at,
event_name,
event_properties
FROM {{ ref('base__app__events') }}

On supprime le bloc is_incremental(), on ajoute event_time pointant vers la colonne de timestamp, et on configure batch_size et lookback pour reproduire la logique précédente.

Lors de la première exécution après la conversion, vous voudrez probablement faire un full refresh pour établir la structure par batches. Pour les très grosses tables, utilisez --event-time-start et --event-time-end pour reconstruire par tranches.

Limites et considérations

Microbatch n’est pas le bon choix pour tous les modèles incrémentaux.

dbt ne garde pas trace des batches déjà traités. Si vous sautez des exécutions, vous aurez des trous dans vos données. C’est le même comportement que l’incrémental traditionnel, mais ça vaut la peine de le garder en tête.

Tous les calculs temporels utilisent UTC. Si votre colonne event_time utilise un autre fuseau horaire, vous devrez gérer la conversion.

La granularité minimale est hour. Pour des données de streaming à haute fréquence, l’incrémental traditionnel reste peut-être nécessaire.

Chaque batch attend la fin du précédent par défaut. concurrent_batches=true existe, mais c’est récent et il peut y avoir des cas limites.

Si un modèle microbatch horaire alimente un modèle microbatch quotidien, il faut une orchestration soignée pour s’assurer que les données upstream sont complètes avant le traitement en aval.

Si vous utilisez le moteur Fusion, microbatch n’est pas encore supporté.

Quand utiliser microbatch

Microbatch convient bien quand :

  • Vos données ont un timestamp clair pour le partitionnement
  • Vous traitez des batches temporels bornés (quotidiens, horaires)
  • Vous avez besoin de backfills simples et intégrés
  • Le retry par batch vous ferait gagner du temps de retraitement
  • Vous voulez simplifier votre logique incrémentale

Restez sur l’incrémental traditionnel quand :

  • Vos données n’ont pas de dimension temporelle naturelle
  • Vous avez besoin d’un traitement infra-horaire
  • Vous faites de la déduplication complexe qui ne rentre pas dans le modèle par batch
  • Vous mettez à jour des enregistrements sur des plages temporelles arbitraires (pas des batches bornés)

Exemple pratique : agrégation de sessions

Voici un exemple complet qui agrège les pages vues en sessions :

{{ config(
materialized='incremental',
incremental_strategy='microbatch',
event_time='session__started_at',
batch_size='day',
lookback=3,
begin='2023-01-01',
partition_by={
"field": "session__started_at",
"data_type": "timestamp",
"granularity": "day"
}
) }}
WITH page_views AS (
SELECT
page_view_id,
session_id,
user_id,
page_url,
viewed_at
FROM {{ ref('base__app__page_views') }}
),
sessions AS (
SELECT
session_id,
user_id,
MIN(viewed_at) AS session__started_at,
MAX(viewed_at) AS session__ended_at,
COUNT(*) AS session__page_views,
TIMESTAMP_DIFF(MAX(viewed_at), MIN(viewed_at), SECOND) AS session__duration_seconds
FROM page_views
GROUP BY session_id, user_id
)
SELECT
session_id,
user_id,
session__started_at,
session__ended_at,
session__page_views,
session__duration_seconds
FROM sessions

Comme base__app__page_views a un event_time configuré, dbt le filtre automatiquement pour chaque batch. Le modèle traite un jour à la fois, en retraitant les trois derniers jours à chaque exécution pour rattraper les événements en retard.

Résumé

Microbatch simplifie les modèles incrémentaux partitionnés par le temps en déplaçant la logique de batch du SQL vers la configuration. La contrepartie, c’est moins de flexibilité : vous êtes contraint à un batching temporel avec une granularité prédéterminée.

Pour les données événementielles et le traitement de séries temporelles, cette contrainte correspond souvent aux besoins réels. Le support intégré du backfill et les retries par batch en font une option à considérer pour les nouveaux modèles incrémentaux.

Pour une logique incrémentale complexe qui ne rentre pas dans le modèle par batch, les patterns traditionnels avec is_incremental() restent le meilleur choix.