Upsert : insérer une ligne, ou mettre à jour celle qui existe

Chaque base utilise une syntaxe UPSERT différente pour les lignes qui existent peut-être déjà. Les exemples appliquent la même modification à une ligne de liste d’envies et comparent le résultat.

Cette page modifie des données, dans votre propre copie de la base de démonstration, celle de votre navigateur. Rechargez la base depuis l’écran d’accueil pour repartir de zéro.

SQLite et PostgreSQL

Les deux utilisent ON CONFLICT, avec la même syntaxe — une requête écrite pour l’un tourne sur l’autre.

1

-- Insert, or update the row that is already there.
INSERT INTO wishlists (customer_id, product_id, added_at)
VALUES (1, 288, '2026-01-01 00:00:00')
ON CONFLICT (customer_id, product_id)
DO UPDATE SET added_at = excluded.added_at;
Ouvrir dans Planche

1 ligne modifiée

Planche s’ouvre dans un onglet séparé, la base d’exemple déjà chargée et cette requête prête dans l’éditeur. Tous les exemples de la page réutilisent cet onglet, et rien ne s’exécute tant que vous n’appuyez pas sur F9.

Vous nommez les colonnes qui entreraient en conflit, et excluded désigne la ligne que vous avez tenté d’insérer. Exécutez-la deux fois : la première est une insertion, la seconde une mise à jour.

2

SELECT customer_id, product_id, added_at
FROM wishlists
WHERE customer_id = 1 AND product_id = 288;
Ouvrir dans Planche

Résultat — 1 ligne

customer_idproduct_idadded_at
12882026-01-01 00:00:00

L’autre branche, c’est « laisser la ligne existante tranquille » :

3

-- "Insert if it is not there, otherwise leave it alone." No error.
INSERT INTO wishlists (customer_id, product_id, added_at)
VALUES (1, 288, '1999-12-31 00:00:00')
ON CONFLICT (customer_id, product_id) DO NOTHING;
Ouvrir dans Planche

0 lignes modifiées

Rien n’a changé, et il n’y a pas eu d’erreur. C’est la différence avec un INSERT ordinaire, qui aurait échoué sur la clé primaire.

PostgreSQL

INSERT INTO wishlists (customer_id, product_id, added_at)
VALUES (1, 288, '2026-01-01 00:00:00')
ON CONFLICT (customer_id, product_id)
DO UPDATE SET added_at = EXCLUDED.added_at;

-- DO NOTHING is the same idea as SQLite's.
INSERT INTO wishlists (customer_id, product_id)
VALUES (1, 288)
ON CONFLICT DO NOTHING;

PostgreSQL — donné à titre de comparaison. La base d’exemple du navigateur est SQLite, cette instruction ne peut donc pas s’exécuter ici.

PostgreSQL écrit excluded en majuscules par convention, et accepte les deux. Il autorise aussi un WHERE sur la branche de mise à jour, pour ne mettre à jour que si une condition est remplie.

MySQL et MariaDB

MySQL / MariaDB

-- MySQL 8.0.20 and later: alias the new row instead of VALUES().
INSERT INTO wishlists (customer_id, product_id, added_at)
VALUES (1, 288, '2026-01-01 00:00:00') AS new
ON DUPLICATE KEY UPDATE added_at = new.added_at;

-- Older MySQL and MariaDB:
INSERT INTO wishlists (customer_id, product_id, added_at)
VALUES (1, 288, '2026-01-01 00:00:00')
ON DUPLICATE KEY UPDATE added_at = VALUES(added_at);

MySQL / MariaDB — donné à titre de comparaison. La base d’exemple du navigateur est SQLite, cette instruction ne peut donc pas s’exécuter ici.

La clause diffère par plus que son nom. Elle réagit à la violation de n’importe quelle contrainte d’unicité, et non d’une que vous désignez : sur une table à plusieurs clés uniques, l’instruction met donc à jour selon celle qui est entrée en conflit — pas forcément celle que vous aviez en tête. MySQL 8.0.20 a déprécié VALUES(col) au profit d’un alias pour la nouvelle ligne : le nouveau code doit utiliser la forme AS new, tandis que MariaDB veut encore VALUES().

SQL Server et Oracle

SQL Server / Oracle

MERGE INTO wishlists AS target
USING (SELECT 1 AS customer_id, 288 AS product_id, '2026-01-01' AS added_at) AS source
    ON target.customer_id = source.customer_id
   AND target.product_id  = source.product_id
WHEN MATCHED THEN
  UPDATE SET added_at = source.added_at
WHEN NOT MATCHED THEN
  INSERT (customer_id, product_id, added_at)
  VALUES (source.customer_id, source.product_id, source.added_at);

SQL Server / Oracle — donné à titre de comparaison. La base d’exemple du navigateur est SQLite, cette instruction ne peut donc pas s’exécuter ici.

MERGE est la réponse du standard SQL, et de loin la plus verbeuse : la ligne entrante s’écrit comme une requête, se joint à la cible, et chaque branche est détaillée. Elle est aussi plus générale — WHEN NOT MATCHED BY SOURCE THEN DELETE en fait une synchronisation complète. Oracle n’a pas besoin du point-virgule final à l’intérieur de PL/SQL et SQL Server l’exige, ce qui est la raison habituelle pour laquelle un MERGE copié échoue du premier coup.

Pour une seule ligne sous SQL Server, beaucoup d’équipes font plutôt un UPDATE suivi d’un INSERT protégé par @@ROWCOUNT dans une transaction, car MERGE traîne une longue liste de bugs signalés autour de la concurrence et des déclencheurs. Si vous l’utilisez là, tenez-le dans une transaction avec le bon niveau d’isolation, et ne le considérez pas comme atomique en soi.

REPLACE n’est pas un upsert

SQLite et MySQL ont tous deux REPLACE, et cela ressemble au raccourci pour faire la même chose. Ce n’est pas la même opération.

4

-- REPLACE deletes the conflicting row and inserts a new one, so
-- added_at is not kept — it falls back to its default. Run the check
-- above again afterwards and compare.
INSERT OR REPLACE INTO wishlists (customer_id, product_id)
VALUES (1, 288);
Ouvrir dans Planche

1 ligne modifiée

Relancez la requête de vérification : added_at n’est plus la date de 2026, c’est l’heure actuelle. REPLACE supprime la ligne en conflit et en insère une nouvelle : toutes les colonnes que vous n’avez pas listées reviennent donc à leur valeur par défaut. Sur une table plus large, cela jette silencieusement de vraies données, et la suppression déclenche les triggers de suppression et se propage aux lignes filles par les clés étrangères.

Ne l’utilisez que si vous voulez vraiment dire « jette l’ancienne ligne ». Pour « insérer ou mettre à jour », utilisez ON CONFLICT.