Upsert-Syntax im Datenbankvergleich: ON DUPLICATE KEY, ON CONFLICT, MERGE
AI generated
SELECT
JOIN
SQL · Datenbankvergleich · Portabilitaet
Upsert-Syntax im Datenbankvergleich
ON DUPLICATE KEY, ON CONFLICT und MERGE nebeneinander

Insert oder Update in einer einzigen Anweisung klingt nach einem einfachen Problem, doch jede Datenbank loest es mit eigener Syntax, eigener Atomaritaet und eigenen Fallstricken. Wer Upsert-Code zwischen MySQL, PostgreSQL, SQL Server, Oracle und SQLite portabel halten will, muss die Unterschiede kennen, bevor der erste produktive Wechsel ansteht.

18 Min. Lesezeit MySQL · PostgreSQL · SQL Server · Oracle · SQLite Upsert · Atomaritaet · Portabilitaet

1. Was Upsert bedeutet und warum jede Datenbank es anders loest

Der Begriff Upsert setzt sich aus Update und Insert zusammen und beschreibt eine einzige Anweisung, die eine Zeile aktualisiert, wenn sie bereits existiert, und sie sonst neu anlegt. In der Anwendungsentwicklung taucht dieses Muster staendig auf: Synchronisation von externen Daten, Caching-Tabellen, Konfigurationswerte, Zaehlerstaende und Import-Routinen brauchen fast immer genau diese Semantik. Ohne native Unterstuetzung muesste man zuerst pruefen, ob eine Zeile existiert, und dann bedingt einfuegen oder aktualisieren, was zwei Round-Trips zur Datenbank und eine Race Condition zwischen beiden Schritten bedeutet.

Der ANSI SQL Standard kennt seit SQL:2003 die MERGE-Anweisung als generischen Mechanismus fuer diese Aufgabe, doch die grossen Datenbanken haben sich lange vor der Standardisierung eigene, teils inkompatible Wege ausgedacht. MySQL hat mit ON DUPLICATE KEY UPDATE eine eigene Erweiterung eingefuehrt, PostgreSQL folgte deutlich spaeter mit ON CONFLICT, SQLite bietet sowohl eine eigene INSERT OR REPLACE-Variante als auch inzwischen ein PostgreSQL-aehnliches ON CONFLICT. Wer Code zwischen diesen Systemen migriert oder eine mehrere Datenbanken unterstuetzende Anwendung schreibt, muss die Unterschiede in Syntax, Semantik und Atomaritaet genau kennen, um keine stillen Bugs zu produzieren.

2. MySQL: INSERT ... ON DUPLICATE KEY UPDATE im Detail

MySQL loest Upsert ueber die Klausel ON DUPLICATE KEY UPDATE, die direkt an ein normales INSERT angehaengt wird. Ein Konflikt entsteht, wenn der einzufuegende Datensatz gegen einen PRIMARY KEY oder einen UNIQUE-Index verstoesst. In diesem Fall wird statt des Inserts die angegebene Update-Klausel ausgefuehrt, wobei mit VALUES(spalte) beziehungsweise ab MySQL 8.0.19 mit einem expliziten Alias auf die urspruenglich einzufuegenden Werte zugegriffen werden kann. Diese Syntax ist proprietaer und findet sich in keiner anderen grossen Datenbank in dieser Form.

Eine Besonderheit von MySQL: Wenn mehrere Unique-Constraints gleichzeitig verletzt werden, ist nicht immer vorhersagbar, gegen welchen Index der Konflikt zuerst ausgeloest wird, was bei Tabellen mit mehreren Unique-Schluesseln zu ueberraschendem Verhalten fuehren kann. Ausserdem zaehlt MySQL affected_rows bei einem tatsaechlichen Update als zwei statt eins, sofern sich mindestens ein Wert aendert, ein Detail, das viele ORM-Integrationen falsch interpretieren, wenn sie die Rueckgabe fuer Erfolgspruefungen nutzen.


-- MySQL: INSERT ... ON DUPLICATE KEY UPDATE
CREATE TABLE product_stock (
  sku VARCHAR(64) PRIMARY KEY,
  quantity INT NOT NULL,
  updated_at DATETIME NOT NULL
);

-- Upsert: insert new SKU or update quantity if it already exists
INSERT INTO product_stock (sku, quantity, updated_at)
VALUES ('SKU-1001', 42, NOW())
AS new_values
ON DUPLICATE KEY UPDATE
  quantity = new_values.quantity,
  updated_at = new_values.updated_at;

-- Older syntax without the row alias (still widely used, MySQL < 8.0.19)
INSERT INTO product_stock (sku, quantity, updated_at)
VALUES ('SKU-1002', 17, NOW())
ON DUPLICATE KEY UPDATE
  quantity = VALUES(quantity),
  updated_at = VALUES(updated_at);

3. PostgreSQL: INSERT ... ON CONFLICT im Detail

PostgreSQL hat seit Version 9.5 mit ON CONFLICT einen deutlich expliziteren Ansatz gewaehlt. Anders als MySQL erlaubt PostgreSQL, den ausloesenden Constraint namentlich oder ueber eine Spaltenliste anzugeben, was bei mehreren Unique-Constraints eindeutiges Verhalten garantiert. Zwei Varianten stehen zur Verfuegung: DO NOTHING ignoriert den Konflikt komplett und laesst die bestehende Zeile unveraendert, DO UPDATE SET fuehrt ein gezieltes Update aus, wobei mit EXCLUDED.spalte auf die eigentlich einzufuegenden Werte zugegriffen wird, analog zu VALUES() in MySQL.

Ein starkes Feature von PostgreSQL ist die Moeglichkeit, in der DO UPDATE-Klausel eine zusaetzliche WHERE-Bedingung anzugeben, sodass ein Update nur dann greift, wenn eine bestimmte Bedingung erfuellt ist, etwa wenn der neue Zeitstempel juenger ist als der gespeicherte. Diese bedingte Aktualisierung ersetzt in vielen Faellen ein zusaetzliches optimistisches Locking und ist in MySQL ohne zusaetzliche Anwendungslogik nicht direkt nachbildbar.


-- PostgreSQL: INSERT ... ON CONFLICT
CREATE TABLE product_stock (
  sku VARCHAR(64) PRIMARY KEY,
  quantity INTEGER NOT NULL,
  updated_at TIMESTAMPTZ NOT NULL
);

-- Upsert with explicit conflict target and EXCLUDED reference
INSERT INTO product_stock (sku, quantity, updated_at)
VALUES ('SKU-1001', 42, now())
ON CONFLICT (sku) DO UPDATE SET
  quantity = EXCLUDED.quantity,
  updated_at = EXCLUDED.updated_at
-- Conditional update: only apply if the incoming row is newer
WHERE product_stock.updated_at < EXCLUDED.updated_at;

-- Ignore duplicates entirely instead of updating
INSERT INTO product_stock (sku, quantity, updated_at)
VALUES ('SKU-1002', 17, now())
ON CONFLICT (sku) DO NOTHING;

4. SQL Server und Oracle: MERGE als Standard-naeherer Ansatz

Sowohl SQL Server als auch Oracle setzen fuer Upsert-Aufgaben auf die deutlich maechtigere, aber auch deutlich verbosere MERGE-Anweisung, die dem ANSI Standard am naechsten kommt. MERGE vergleicht eine Zieltabelle mit einer Quelle ueber eine ON-Bedingung und erlaubt separate Zweige fuer WHEN MATCHED, WHEN NOT MATCHED und in Oracle zusaetzlich WHEN NOT MATCHED BY SOURCE, wodurch sich auch Loeschungen von Zeilen integrieren lassen, die in der Quelle nicht mehr vorkommen. Diese Flexibilitaet macht MERGE zum Werkzeug der Wahl fuer komplexe Synchronisationsaufgaben zwischen zwei Tabellen, nicht nur fuer einzelne Zeilen.

Der Preis dieser Flexibilitaet ist deutlich mehr Boilerplate-Code fuer den einfachen Fall einer einzelnen Zeile, und SQL Server hatte in aelteren Versionen dokumentierte Race-Condition-Probleme bei hoher Nebenlaeufigkeit, wenn MERGE ohne zusaetzliche Transaktionsisolation verwendet wurde. Microsoft empfiehlt seit mehreren Versionen explizit, bei reinem Upsert-Bedarf auf einfachere Muster mit UPDATE gefolgt von einem bedingten INSERT innerhalb derselben Transaktion auszuweichen, sofern keine komplexe Mehrzeilen-Synchronisation noetig ist.


-- SQL Server / Oracle: MERGE for upsert semantics
MERGE INTO product_stock AS target
USING (VALUES ('SKU-1001', 42)) AS source (sku, quantity)
ON target.sku = source.sku
WHEN MATCHED THEN
  UPDATE SET
    target.quantity = source.quantity,
    target.updated_at = SYSDATETIME()
WHEN NOT MATCHED THEN
  INSERT (sku, quantity, updated_at)
  VALUES (source.sku, source.quantity, SYSDATETIME());

-- Oracle variant uses SYSTIMESTAMP instead of SYSDATETIME
-- and requires a terminating semicolon on the MERGE statement itself

5. SQLite: INSERT OR REPLACE und sein ON CONFLICT Pendant

SQLite bringt mit INSERT OR REPLACE eine eigene, aeltere Loesung mit, die bei einem Constraint-Verstoss die bestehende Zeile vollstaendig loescht und durch die neue ersetzt, statt einzelne Spalten zu aktualisieren. Das klingt nach demselben Ergebnis wie ein Update, hat aber einen entscheidenden Unterschied: Loeschen und Neuanlegen loest verknuepfte Trigger und Foreign-Key-Kaskaden aus, die bei einem echten Update nie greifen wuerden, und ein AUTOINCREMENT-Wert der geloeschten Zeile geht verloren, wenn kein expliziter Primary-Key-Wert angegeben wird.

Seit SQLite 3.24 unterstuetzt die Datenbank zusaetzlich eine PostgreSQL-kompatible ON CONFLICT-Syntax mit DO UPDATE SET und excluded.spalte, die genau die Probleme von INSERT OR REPLACE vermeidet, weil sie eine echte gezielte Aktualisierung ausfuehrt statt Loeschen und Neuanlegen. Fuer neue SQLite-Projekte ist die ON CONFLICT-Variante daher fast immer vorzuziehen, insbesondere wenn Trigger oder Fremdschluesselbeziehungen im Spiel sind.


-- SQLite: legacy INSERT OR REPLACE (delete-then-insert semantics)
INSERT OR REPLACE INTO product_stock (sku, quantity, updated_at)
VALUES ('SKU-1001', 42, datetime('now'));
-- Warning: triggers a DELETE and INSERT internally, fires related triggers twice

-- SQLite 3.24+: PostgreSQL-compatible ON CONFLICT, true update semantics
INSERT INTO product_stock (sku, quantity, updated_at)
VALUES ('SKU-1001', 42, datetime('now'))
ON CONFLICT(sku) DO UPDATE SET
  quantity = excluded.quantity,
  updated_at = excluded.updated_at;

6. Race Conditions und warum Upsert nicht gleich Upsert ist

Der eigentliche Grund, Upsert-Syntax in der Datenbank statt in der Anwendungsschicht zu implementieren, ist Atomaritaet. Ein manuelles Muster aus SELECT, gefolgt von bedingtem UPDATE oder INSERT, hat zwischen den beiden Anweisungen ein Zeitfenster, in dem eine parallele Transaktion dieselbe Zeile einfuegt. Ohne zusaetzliches Locking fuehrt das entweder zu einem Constraint-Fehler oder zu einer doppelten Zeile, je nachdem, ob ein Unique-Constraint existiert. Native Upsert-Anweisungen fuehren die gesamte Pruefung und Aktualisierung innerhalb eines einzigen atomaren Server-seitigen Vorgangs aus und schliessen dieses Zeitfenster vollstaendig.

Wichtig ist trotzdem, das Isolationslevel der Transaktion im Blick zu behalten. PostgreSQL garantiert bei ON CONFLICT Atomaritaet auch unter REPEATABLE READ, MySQL mit InnoDB benoetigt bei ON DUPLICATE KEY UPDATE teilweise zusaetzliche Gap Locks, die unter hoher Nebenlaeufigkeit zu mehr Deadlocks fuehren koennen als erwartet. Wer Upsert-Operationen in Batches mit hoher Parallelitaet ausfuehrt, sollte die jeweilige Locking-Dokumentation der eingesetzten Datenbank pruefen, statt Atomaritaet blind vorauszusetzen.

7. Portable Anwendungscode-Strategien

Wer eine Anwendung schreibt, die mehrere Datenbanken unterstuetzen soll, sollte Upsert-Logik hinter einer eigenen Abstraktionsschicht verstecken, statt die native Syntax direkt in Geschaeftslogik einzubetten. Ein gaengiges Muster: eine Repository-Methode upsert(tabelle, schluessel, werte), die intern je nach Datenbanktreiber die passende Syntax generiert. Die meisten modernen ORMs wie Doctrine, Eloquent oder SQLAlchemy bieten bereits eine solche Abstraktion, deren generierte Anweisungen sich aber im Detail unterscheiden koennen, weshalb ein Blick in den generierten SQL-Code vor dem produktiven Einsatz sinnvoll ist.

Ist eine ORM-Abstraktion nicht verfuegbar oder zu ungenau, hilft eine Feature-Detection zur Laufzeit anhand der Treiber-Metadaten, kombiniert mit datenbankspezifischen SQL-Templates pro unterstuetztem System. Wichtig ist, in Tests explizit den Konfliktfall zu pruefen, nicht nur den Neuanlage-Fall, da genau dieser Zweig zwischen den Datenbanken am staerksten variiert und in der Praxis am seltensten automatisiert getestet wird.


-- Portable pattern: explicit two-step upsert as a database-agnostic fallback
-- Works identically on any SQL database that supports transactions
BEGIN;

UPDATE product_stock
SET quantity = 42, updated_at = CURRENT_TIMESTAMP
WHERE sku = 'SKU-1001';

-- If no row was updated, the row does not exist yet, insert it
INSERT INTO product_stock (sku, quantity, updated_at)
SELECT 'SKU-1001', 42, CURRENT_TIMESTAMP
WHERE NOT EXISTS (
  SELECT 1 FROM product_stock WHERE sku = 'SKU-1001'
);

COMMIT;
-- Note: still needs SERIALIZABLE isolation or a unique constraint
-- plus retry-on-conflict logic to be fully race-condition-safe

8. Performance-Unterschiede zwischen den Upsert-Varianten

Bei grossen Datenmengen zeigen sich deutliche Performance-Unterschiede zwischen den Ansaetzen. MySQLs ON DUPLICATE KEY UPDATE ist fuer Batch-Inserts mit vielen Werten in einer einzigen Anweisung gut optimiert und skaliert linear mit der Anzahl der Zeilen. PostgreSQLs ON CONFLICT verhaelt sich aehnlich effizient, hat aber bei sehr breiten Tabellen mit vielen zu aktualisierenden Spalten einen messbaren Overhead durch die interne Pruefung des Konflikts vor dem eigentlichen Schreibvorgang.

MERGE in SQL Server und Oracle ist fuer Mehrzeilen-Synchronisation optimiert, hat aber fuer einzelne Zeilen einen hoeheren Parsing- und Planungs-Overhead, da der Optimizer stets die volle Join-Semantik zwischen Ziel und Quelle bewerten muss. INSERT OR REPLACE in SQLite ist durch das interne Loeschen und Neuanlegen bei Tabellen mit vielen Fremdschluesselbeziehungen tendenziell langsamer als die neuere ON CONFLICT-Variante, da zusaetzliche Kaskaden-Pruefungen ausgeloest werden, die bei einem echten Update entfallen wuerden.

9. Upsert-Syntax im direkten Vergleich

Die folgende Tabelle fasst die zentralen Unterschiede der besprochenen Upsert-Mechanismen zusammen und zeigt, worauf bei einer Migration zwischen den Systemen besonders zu achten ist.

Datenbank Syntax Update-Semantik Besonderheit
MySQL ON DUPLICATE KEY UPDATE Echtes Update Kein expliziter Conflict-Target moeglich
PostgreSQL ON CONFLICT ... DO UPDATE Echtes Update Bedingtes Update mit WHERE moeglich
SQL Server / Oracle MERGE ... WHEN MATCHED Echtes Update Mehrzeilen-Synchronisation inklusive Delete-Zweig
SQLite (legacy) INSERT OR REPLACE Delete plus Insert Loest Trigger und Kaskaden doppelt aus
SQLite (3.24+) ON CONFLICT ... DO UPDATE Echtes Update PostgreSQL-kompatible Syntax

Wer zwischen diesen Systemen migriert, sollte insbesondere auf die Delete-plus-Insert-Semantik von INSERT OR REPLACE achten, da Fremdschluesselbeziehungen und Trigger sich dabei fundamental anders verhalten als bei einem echten Update. Bei SQL Server und Oracle lohnt sich zusaetzlich ein Blick auf das gemeldete MERGE-Verhalten unter hoher Nebenlaeufigkeit, bevor die Anweisung fuer kritische Zaehlerstaende eingesetzt wird.

Mironsoft

Datenbank-Architektur, Migrationen und portables SQL

Upsert-Logik, die auf jeder Zieldatenbank verlaesslich laeuft?

Wir pruefen bestehenden Upsert-Code auf Race Conditions und Portabilitaetsluecken und bauen eine saubere Abstraktionsschicht, die MySQL, PostgreSQL, SQL Server und SQLite gleichermassen korrekt bedient.

Code-Review

Analyse bestehender Upsert-Statements auf Atomaritaet und Portabilitaet

Migration

Umbau von INSERT OR REPLACE oder MERGE auf zielsystemgerechte Syntax

Abstraktionsschicht

Repository-Pattern fuer datenbankuebergreifende Upsert-Aufrufe

10. Zusammenfassung

Die Upsert-Syntax unterscheidet sich zwischen MySQL, PostgreSQL, SQL Server, Oracle und SQLite so stark, dass ein direkter Copy-Paste-Ansatz bei Migrationen fast immer scheitert. MySQL nutzt die proprietaere Klausel ON DUPLICATE KEY UPDATE, PostgreSQL setzt auf das flexiblere ON CONFLICT mit expliziten Conflict-Targets und bedingten Updates, SQL Server und Oracle folgen dem ANSI-naeheren, aber deutlich verboseren MERGE, und SQLite bietet sowohl eine aeltere Delete-plus-Insert-Variante als auch ein modernes, PostgreSQL-kompatibles ON CONFLICT.

Fuer produktiven Code lohnt sich eine Abstraktionsschicht, die die jeweilige Upsert-Syntax hinter einer einheitlichen Schnittstelle verbirgt, kombiniert mit expliziten Tests fuer den Konfliktfall und einer bewussten Entscheidung fuer das passende Isolationslevel. Wer diese Upsert-Syntax-Unterschiede kennt, vermeidet stille Bugs bei Datenbankwechseln und schreibt von Anfang an portableren SQL-Code.

Upsert-Syntax im Datenbankvergleich — Das Wichtigste auf einen Blick

MySQL

ON DUPLICATE KEY UPDATE mit VALUES() oder Row-Alias, kein expliziter Conflict-Target.

PostgreSQL

ON CONFLICT mit EXCLUDED-Referenz und optionaler bedingter Update-Klausel.

SQL Server / Oracle

MERGE fuer Mehrzeilen-Synchronisation, mehr Boilerplate fuer Einzelzeilen.

SQLite

ON CONFLICT statt INSERT OR REPLACE bevorzugen, um Trigger-Doppelausloesung zu vermeiden.

11. FAQ: Upsert-Syntax im Datenbankvergleich

1Was bedeutet Upsert in SQL?
Insert oder Update in einer einzigen atomaren Anweisung: existiert die Zeile schon, wird sie aktualisiert, sonst neu eingefuegt.
2Warum keine einheitliche Syntax im Standard?
MERGE existiert seit SQL:2003, aber MySQL und PostgreSQL hatten eigene Loesungen bereits vorher und behielten sie aus Kompatibilitaetsgruenden bei.
3Ist ON DUPLICATE KEY UPDATE atomar?
Ja, komplett server-seitig. Bei hoher Nebenlaeufigkeit koennen zusaetzliche Gap Locks von InnoDB dennoch zu mehr Deadlocks fuehren.
4Vorteil von ON CONFLICT?
Expliziter Conflict-Target plus zusaetzliche WHERE-Bedingung im Update-Zweig, ohne separates Locking.
5Warum ist INSERT OR REPLACE riskant?
Loescht intern und legt neu an statt zu aktualisieren, dadurch feuern Trigger doppelt und AUTOINCREMENT-Werte koennen verloren gehen.
6Wann MERGE statt Upsert verwenden?
Bei Mehrzeilen-Synchronisation zwischen zwei Tabellen inklusive Loeschungen. Fuer Einzelzeilen meist mehr Aufwand als noetig.
7Wie schreibe ich portablen Upsert-Code?
Hinter einer Abstraktionsschicht mit treiberspezifischer Syntax-Generierung und expliziten Tests fuer den Konfliktfall.
8EXCLUDED und VALUES() austauschbar?
Nein, EXCLUDED ist PostgreSQL- und SQLite-spezifisch, VALUES() beziehungsweise Row-Alias ist MySQL-spezifisch.
9Beeinflusst Upsert das Isolationslevel?
Die Anweisung selbst ist atomar, das Isolationslevel wirkt sich aber auf Nebenlaeufigkeit und Deadlock-Wahrscheinlichkeit bei Batch-Upserts aus.
10Mehrere verletzte Unique-Constraints in MySQL?
MySQL waehlt nicht immer vorhersagbar einen Index aus. PostgreSQLs expliziter Conflict-Target vermeidet dieses Problem.