> ## Content Index
> Fetch the complete content index at: https://www.idriss-benbassou.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# MERGE SQL : INSERT, UPDATE, DELETE en une requête (Snowflake, dbt, BigQuery)
- URL: https://www.idriss-benbassou.com/merge-sql-insert-update-delete-en-une-requete-snowflake-dbt-bigquery/
- Published: 2026-05-24T12:21:45.000Z
- Updated: 2026-05-26T20:33:06.000Z
- Author: Idriss BENBASSOU
- Tags: SQL, Snowflake, #rating-2, dbt

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](https://www.idriss-benbassou.com/streams-snowflake-suivre-les-changements-de-donnees-cdc/) ou [dbt](https://www.idriss-benbassou.com/dbt-data-build-tool-a-quoi-ca-sert-exemples-sql/).

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 :

```sql
-- 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

```sql
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` :

```sql
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`.

```sql
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 :

```sql
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 :

```sql
{{ 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 |
| ------------------------------ | --------- | -------- | ---------- |
| WHEN MATCHED (UPDATE / DELETE) | ✅         | ✅        | ✅          |
| WHEN MATCHED AND <condition>   | ✅         | ✅        | ✅          |
| Plusieurs WHEN MATCHED         | ✅         | ✅        | ⚠️ limité  |
| WHEN NOT MATCHED BY SOURCE     | ❌         | ✅        | ✅          |

💡

**`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"](https://www.idriss-benbassou.com/scd-slowly-changing-dimensions-type-1-2-3-6-sql-dbt-snowflake/) 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](https://datacertification.fr/apprendre-sql?ref=idriss-benbassou.com) 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](https://datacertification.fr/apprendre-sql?ref=idriss-benbassou.com)

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](https://www.idriss-benbassou.com/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](https://www.idriss-benbassou.com/scd-slowly-changing-dimensions-type-1-2-3-6-sql-dbt-snowflake/).

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

👉 [Réserver un appel de 30 minutes](https://calendly.com/idriss-benbassou-datavio/30min?ref=idriss-benbassou.com)

## 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](https://www.idriss-benbassou.com/transactions-sql-definition-quand-les-utiliser/). 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](https://www.idriss-benbassou.com/transactions-sql-definition-quand-les-utiliser/) : 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.

#### 

####