Dans l'article sur les matérialisations, on a survolé l'incremental en disant "on y reviendra". On y est.

Jusqu'ici, les modèles table se reconstruisent entièrement à chaque dbt run. Sur une petite table, aucun souci. Sur une table de commandes ou d'événements qui grossit tous les jours, recalculer tout l'historique chaque nuit devient lent et cher. L'incremental règle ça car il ne traite que les nouvelles lignes.

Le problème que résout l'incremental

Imagine une table de commandes avec deux ans d'historique. Chaque nuit, ton dbt run la reconstruit en entier car il relit deux ans de données pour ajouter les commandes d'hier. C'est absurde. 99% du calcul porte sur des lignes qui n'ont pas bougé.

Un modèle incremental inverse la logique. La première fois, il construit toute la table avec un full reload. Ensuite, à chaque exécution, il ne traite que les lignes nouvelles ou modifiées. Tu passes d'un recalcul complet à un traitement de quelques milliers de lignes. Le gain en temps et en coût compute est énorme.

Comment ça marche

Trois éléments :

  • La matérialisation incremental : tu la déclares dans le config du modèle,
  • La macro is_incremental() : un filtre qui ne s'applique qu'aux exécutions incrémentales, pas au premier run ni à une reconstruction complète,
  • {{ this }} : une référence à la table elle-même, celle qui existe déjà, pour savoir ce qu'elle contient déjà.

L'idée est simple, au premier run, dbt construit toute la table et aux runs suivants, il regarde ce qui est déjà dans {{ this }} et ne charge que ce qui manque.

Notre premier modèle incrémental (incremental load)

D'abord, un petit ajustement. Pour savoir quelles lignes sont nouvelles, on a besoin de la date d'arrivée de la donnée. On a justement la colonne _loaded_at dans raw.orders, mais notre stg_orders ne la fait pas remonter. Ajoute-la au select de stg_orders.sql :

    renamed as (
        select
            order_id,
            customer_id,
            amount      as montant,
            status      as statut,
            order_date  as date_commande,
            _loaded_at
        from source
    )

Maintenant, crée un modèle models/marts/mart_orders.sql :

{{ config(
    materialized='incremental',
    unique_key='order_id'
) }}

select
    order_id,
    customer_id,
    montant,
    statut,
    date_commande,
    _loaded_at
from {{ ref('stg_orders') }}

{% if is_incremental() %}

  -- ce filtre ne s'applique QUE aux runs incrémentaux
  where _loaded_at > (select max(_loaded_at) from {{ this }})

{% endif %}

Décortiquons le bloc is_incremental(). Au premier dbt run, la table n'existe pas encore, donc is_incremental() renvoie faux : le filtre est ignoré et dbt construit toute la table. Aux runs suivants, la table existe, is_incremental() renvoie vrai, et le filtre where _loaded_at > (select max(_loaded_at) from {{ this }}) ne garde que les lignes arrivées après la dernière déjà chargée.

On va tout lancer la première fois :

dbt run

Toute la table est construite. Maintenant, simule l'arrivée d'une nouvelle commande dans la source :

INSERT INTO raw.orders (order_id, customer_id, amount, status, order_date)
VALUES (200, 2, 59.90, 'completed', '2026-04-01');

Relance le même dbt run --select mart_orders. Cette fois, dbt ne traite qu'une seule ligne, la nouvelle. Il n'a pas retouché aux autres. C'est exactement ça l'incremental.

La clé unique : gérer les mises à jour

Tu as remarqué le unique_key='order_id' dans le config. C'est important, et voici pourquoi.

Sans clé unique, un modèle incremental ne fait qu'ajouter les nouvelles lignes.

Avec unique_key='order_id', dbt fait un upsert et donc si la clé existe déjà, il met la ligne à jour, sinon, il l'insère. Aucun risque d'avoir des doublons et c'est ce qu'on veut dans la majorité des cas.

Le MERGE

Voilà le lien que je te promettais dans l'article MERGE SQL. Sur Snowflake, quand tu déclares un modèle incremental avec une unique_key, dbt génère pour toi une instruction... MERGE.

dbt prend ton select, le compare à la table existante sur la clé unique, et écrit le MERGE qui met à jour les lignes correspondantes et insère les nouvelles. Tu écris un SELECT simple, dbt s'occupe de la mécanique d'upsert. Si tu veux comprendre ce qui se passe vraiment, lis ou relis l'article MERGE, c'est littéralement la requête SQL que dbt produit ici.

Cette mécanique s'appelle une stratégie incrémentale, et le merge est celle par défaut sur Snowflake. Il en existe d'autres (append, delete+insert, insert_overwrite) qu'on choisit selon le cas, mais le merge couvre la grande majorité des besoins.

Faire un full refresh

Parfois, tu veux repartir de zéro ou tu dois recharger tout l'historique. Le filtre is_incremental() t'en empêcherait, puisqu'il ne regarde que le nouveau. Pour le contourner, tu forces une reconstruction complète avec

dbt run --select mart_orders --full-refresh

Avec --full-refresh, dbt ignore le bloc is_incremental() et reconstruit la table entièrement, comme au premier run.

Gérer les changements de colonnes

Un dernier point. Le jour où tu ajoutes une colonne à ton modèle incremental, que devient la table existante ? Par défaut, dbt ignore le changement, et ta nouvelle colonne n'apparaît pas tant que tu ne fais pas un full-refresh.

Tu peux contrôler ce comportement avec on_schema_change dans le config :

{{ config(
    materialized='incremental',
    unique_key='order_id',
    on_schema_change='append_new_columns'
) }}

append_new_columns ajoute automatiquement les nouvelles colonnes sans tout reconstruire. Les autres valeurs possibles sont sync_all_columns, fail et ignore.

⚠️
Attention au piège : on_schema_change gère seulement la structure de la table, pas son contenu. Avec append_new_columns, la colonne est bien ajoutée, mais les lignes déjà chargées la gardent à NULL, car le run incrémental ne les retouche pas. Seules les nouvelles lignes ont la valeur calculée. Pour remplir la colonne sur tout l'historique, il faut un --full-refresh.

Quand l'utiliser, et quand surtout pas

Le chargement incremental est puissant, mais c'est aussi le plus piégeux des modes donc il faut le réserver aux cas qui le justifient :

  • Oui : grosses tables qui grossissent surtout par ajout (commandes, événements, logs), là où reconstruire tout coûte vraiment cher.
  • Non : petites tables (une vue ou une table normale suffit), logique qui change souvent, ou quand tu veux juste de la simplicité.

L'incremental ajoute une vraie complexité. Le filtre simple where _loaded_at > max(...) qu'on a écrit peut rater une ligne arrivée juste après le dernier run, et cette ligne est alors perdue. En production, on ajoute souvent une petite fenêtre de recouvrement pour éviter cela, mais garde toujours ce risque en tête.

La règle est simple, il faut passer un modèle en incremental que quand le volume le justifie vraiment. Sur une table de quelques milliers de lignes, une simple table est plus simple, plus sûre, et largement assez rapide.

La suite ?

Récap. de ce qu'on a vu dans cet article :

  • Comprendre pourquoi reconstruire une grosse table à chaque run n'est pas une bonne idée
  • Créer un modèle incremental avec materialized='incremental' et le bloc is_incremental()
  • Utiliser unique_key pour faire un upsert et éviter les doublons
  • Voir que dbt génère en réalité un MERGE sur Snowflake
  • Apprendre à tout reconstruire avec --full-refresh et à gérer les colonnes avec on_schema_change
  • Poser la règle : l'incremental seulement quand le volume le justifie.

Dans le prochain article, on reste dans la logique de chargement intelligent mais on s'attaque à un autre besoin et c'est gardé l'historique des changements. Ce seront les snapshots dbt, l'implémentation native du SCD Type 2.


Aller plus loin

Pour comprendre ce que coûte vraiment un recalcul complet côté Snowflake (compute, micro-partitions, clustering), tout est dans le parcours.

👉 Accéder à la Formation Snowflake

Et pour t'entraîner sur dbt en conditions d'examen, la certification dbt Analytics Engineering teste précisément les modèles incrementals et leurs stratégies.

👉 Préparer la certification dbt sur DataCertification.fr

Tu veux que je t'accompagne sur ton projet data (Snowflake, dbt, modélisation, industrialisation) ?

👉 Réserver un appel de 30 minutes


Questions fréquentes

C'est quoi un modèle incremental en dbt ?

Un modèle incremental est un modèle qui construit sa table une première fois, puis ne traite que les lignes nouvelles ou modifiées aux exécutions suivantes, au lieu de tout reconstruire. On le déclare avec materialized='incremental' et c'est idéal pour les grosses tables qui grossissent par ajout, comme les commandes, les événements ou les logs.

À quoi sert is_incremental() en dbt ?

is_incremental() est une macro qui renvoie vrai uniquement lors d'une exécution incrémentale, c'est-à-dire quand la table existe déjà et qu'on n'est pas en reconstruction complète. On l'utilise pour entourer un filtre qui ne garde que les nouvelles lignes. Au premier run et en full-refresh, elle renvoie faux et le filtre est ignoré, donc toute la table est construite.

À quoi sert la unique_key dans un modèle incremental ?

La unique_key transforme le chargement en upsert. Sans elle, dbt ne fait qu'ajouter les nouvelles lignes, ce qui peut créer des doublons et ne met pas à jour les lignes modifiées. Avec elle, dbt met à jour la ligne si la clé existe déjà et l'insère sinon. Sur Snowflake, c'est cette clé qui déclenche la génération d'un MERGE.

Comment reconstruire entièrement un modèle incremental ?

Avec l'option --full-refresh, par exemple dbt run --select mon_modele --full-refresh. dbt ignore alors le bloc is_incremental() et reconstruit toute la table comme au premier run. C'est utile après un changement de logique ou pour recharger tout l'historique.

Quand utiliser un modèle incremental plutôt qu'une table ?

Quand la table est grosse, et que la reconstruire en entier à chaque run coûte trop cher en temps et en compute. Pour une petite table, ou quand la logique change souvent, une simple table est plus simple et plus sûre. L'incremental ajoute de la complexité (donnée en retard, backfills, changements de schéma), donc on ne l'utilise que quand le volume le justifie.