Quand tu charges des données dans une table en mode incrémental, tu te retrouves toujours avec la même question. Est-ce que il faut faire un UPDATE ou un INSERT.

Le MERGE répond à ça et c'est l'instruction qu'on retrouve partout dans le chargement incrémental, avec Streams Snowflake ou dbt.

On va voir la syntaxe, trois cas concrets, et les pièges à connaître.

Le dataset

On part d'une table cible clients et d'une source clients_maj qui contient les changements du jour :

-- Table cible : clients
| id | nom    | ville  |
| 1  | Marc   | Paris  |
| 2  | Sophie | Lyon   |

-- Table source : clients_maj
| id | nom    | ville     |
| 1  | Marc   | Bordeaux  |   -- Marc a déménagé
| 3  | Karim  | Marseille |   -- nouveau client

L'objectif : mettre à jour Marc, insérer Karim, ne pas toucher Sophie.

La syntaxe du MERGE

MERGE INTO clients AS t
USING clients_maj AS s
ON t.id = s.id
WHEN MATCHED THEN UPDATE SET
    nom   = s.nom,
    ville = s.ville
WHEN NOT MATCHED THEN INSERT (id, nom, ville)
    VALUES (s.id, s.nom, s.ville);
  • ON t.id = s.id : la clé qui dit si une ligne de la source existe déjà dans la cible.
  • WHEN MATCHED : la ligne existe des deux côtés, on la met à jour.
  • WHEN NOT MATCHED : la ligne est dans la source mais pas dans la cible, on l'insère.

Résultat dans clients :

id nom ville
1 Marc Bordeaux
2 Sophie Lyon
3 Karim Marseille

Marc mis à jour, Karim inséré, Sophie intacte.

Mettre à jour seulement si la donnée a changé

Si la ligne source est identique à la cible, l'UPDATE ne sert à rien. Et en Snowflake, un UPDATE réécrit des micro-partitions, donc ça coûte du compute pour rien. On ajoute une condition au WHEN MATCHED :

WHEN MATCHED AND (
    t.nom   IS DISTINCT FROM s.nom
    OR t.ville IS DISTINCT FROM s.ville
) THEN UPDATE SET
    nom   = s.nom,
    ville = s.ville

On utilise IS DISTINCT FROM et pas !=, parce que != ne gère pas les NULL. Si t.ville vaut NULL, alors t.ville != s.ville ne renvoie pas TRUE, et la ligne passe à travers sans être mise à jour.

Supprimer avec DELETE WHEN MATCHED

Le WHEN MATCHED ne fait pas que des UPDATE. Il peut aussi déclencher un DELETE. Par exemple, on veut retirer de la table les clients que la source marque comme inactif.

MERGE INTO clients AS t
USING clients_maj AS s
ON t.id = s.id
WHEN MATCHED AND s.statut = 'inactif' THEN DELETE
WHEN MATCHED THEN UPDATE SET
    nom   = s.nom,
    ville = s.ville
WHEN NOT MATCHED THEN INSERT (id, nom, ville)
    VALUES (s.id, s.nom, s.ville);
💡
Attention car l'ordre des WHEN compte. Le moteur lit de haut en bas et applique la première branche qui matche. Ici on traite les inactif en premier, le reste ensuite.

Les doublons dans la source

Si plusieurs lignes source matchent la même ligne cible pour un UPDATE ou un DELETE, le MERGE devient non déterministe. Snowflake échoue par défaut avec ERROR_ON_NONDETERMINISTIC_MERGE = TRUE, et BigQuery renvoie aussi une erreur du type UPDATE/MERGE must match at most one source row for each target row. Pour éviter ça, la source doit être dédoublonnée avant le MERGE.

La règle à retenir c'est qu'une ligne de la cible ne doit matcher qu'une seule ligne de la source. Donc on dédoublonne la source avant :

USING (
    SELECT id, nom, ville
    FROM clients_maj
    QUALIFY ROW_NUMBER() OVER (PARTITION BY id ORDER BY updated_at DESC) = 1
) AS s

QUALIFY ROW_NUMBER() garde une seule ligne par id, la plus récente. C'est le réflexe à avoir dès que ta source peut contenir plusieurs versions d'une même clé.

MERGE avec dbt

Si tu utilises dbt, tu n'écris pas le MERGE toi-même. La matérialisation incremental avec la stratégie merge le génère pour toi :

{{ config(
    materialized='incremental',
    unique_key='id',
    incremental_strategy='merge'
) }}

SELECT id, nom, ville, updated_at
FROM {{ source('app', 'clients') }}

{% if is_incremental() %}
    WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }})
{% endif %}

Tu déclares unique_key, dbt construit le MERGE. Le bloc is_incremental() filtre la source pour ne traiter que les nouvelles lignes à chaque run.

Les variantes par moteur

Le MERGE fait partie du SQL standard, mais les détails changent selon le moteur :

Feature Snowflake BigQuery SQL Server Postgres
WHEN MATCHED (UPDATE / DELETE) ✅ v15+
WHEN MATCHED AND <condition> ✅ v15+
Plusieurs WHEN MATCHED ⚠️ limité ✅ v15+
WHEN NOT MATCHED BY SOURCE ✅ v17+
💡
WHEN NOT MATCHED BY SOURCE sert à traiter les lignes de la cible absentes de la source (pour les supprimer ou les marquer). Pratique pour un full reload, mais attention pas disponible sur Snowflake.

Erreurs courantes

Erreur Solution
Doublons dans la source Dédoublonner avec QUALIFY ROW_NUMBER()...
!= sur des colonnes nullable Utiliser IS DISTINCT FROM
Source non filtrée, MERGE scan tout Filtrer sur updated_at ou consommer un Stream
MERGE pour un simple UPDATE Utiliser UPDATE directement, c'est plus rapide

Aller plus loin

Pour faire de l'article précédent "SCD Type 1, 2, 3, 6 : historiser les dimensions en SQL" Le MERGE est l'instruction de base du chargement incrémental. Il gère bien l'upsert, ce qui correspond à un SCD Type 1. Pour un SCD Type 2, il faut une logique en plus pour expirer l'ancienne version et insérer la nouvelle.

Sur DataCertification.fr, le module SQL Lab propose des exercices MERGE interactifs avec validation automatique. Les certifications SnowPro Core et dbt Analytics Engineering testent toutes les deux le MERGE.

👉 Pratiquer ces patterns sur DataCertification.fr

Pour aller plus loin sur Snowflake en général, j'ai regroupé tous mes articles dans un parcours complet.

👉 Accéder à la Formation Snowflake

Pour les patterns de modélisation qui s'appuient sur MERGE, voir SCD Type 1, 2, 3, 6 : historiser les dimensions en SQL.

Tu veux que je t'accompagne sur ton projet data (pipelines incrémentaux, CDC, dbt) ?

👉 Réserver un appel de 30 minutes

Questions fréquentes

C'est quoi le MERGE en SQL ?

Le MERGE est une instruction qui combine INSERT, UPDATE et DELETE en une seule requête atomique. Tu lui passes une table cible, une source et une condition de jointure. Il applique la bonne opération ligne par ligne, selon que la ligne existe déjà ou pas dans la cible.

Quelle différence entre MERGE et UPSERT ?

UPSERT est le nom générique de l'opération "insert or update". MERGE est la syntaxe SQL standard qui fait ça, et qui gère aussi le DELETE. Postgres a aussi INSERT ... ON CONFLICT, MySQL a INSERT ... ON DUPLICATE KEY UPDATE.

MERGE est-il atomique ?

Oui. Le MERGE est un seul statement : soit toutes les opérations passent, soit aucune. Dans une transaction explicite (BEGIN/COMMIT), il n'est validé qu'au COMMIT.

Comment éviter l'erreur "Duplicate row detected" en Snowflake ?

Cette erreur arrive quand plusieurs lignes de la source matchent la même ligne de la cible. Dédoublonne la source en amont avec QUALIFY ROW_NUMBER() OVER (PARTITION BY clé ORDER BY updated_at DESC) = 1 pour ne garder que la version la plus récente de chaque clé.

Comment utiliser MERGE avec dbt ?

dbt génère le MERGE automatiquement avec la matérialisation incremental et la stratégie merge (par défaut sur Snowflake). Tu déclares unique_key pour la clé de jointure, et tu filtres la source dans un bloc is_incremental(). dbt construit le reste.