Upsert: eine Zeile einfügen oder die vorhandene aktualisieren

Für Zeilen, die möglicherweise bereits existieren, hat jede Datenbank eine eigene UPSERT-Syntax. Die Beispiele führen dieselbe Änderung an einer Wunschlistenzeile aus und vergleichen das Ergebnis.

Diese Seite ändert Daten, und zwar in Ihrer eigenen Browser-Kopie der Beispieldatenbank. Laden Sie das Beispiel über den Willkommensbildschirm neu, um von vorn zu beginnen.

SQLite und PostgreSQL

Beide verwenden ON CONFLICT, und die Syntax ist identisch – eine für die eine geschriebene Abfrage läuft auf der anderen.

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;
In Planche ausführen

1 Zeile geändert

Öffnet Planche in einem eigenen Browser-Tab, mit geladener Beispieldatenbank und dieser Abfrage im Editor. Alle Beispiele der Seite verwenden denselben Tab weiter, und ausgeführt wird erst, wenn Sie F9 drücken.

Sie nennen die Spalten, die in Konflikt geraten würden, und excluded ist die Zeile, die Sie einzufügen versucht haben. Führen Sie es zweimal aus: Der erste Lauf ist ein Insert, der zweite ein Update.

2

SELECT customer_id, product_id, added_at
FROM wishlists
WHERE customer_id = 1 AND product_id = 288;
In Planche ausführen

Ergebnis — 1 Zeile

customer_idproduct_idadded_at
12882026-01-01 00:00:00

Der andere Zweig lautet "vorhandene Zeile in Ruhe lassen":

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;
In Planche ausführen

0 Zeilen geändert

Nichts hat sich geändert, und es gab keinen Fehler. Genau das ist der Unterschied zu einem einfachen INSERT, das am Primärschlüssel gescheitert wäre.

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 — steht hier zum Vergleich. Die Beispieldatenbank im Browser ist SQLite, deshalb lässt sich diese Anweisung hier nicht ausführen.

PostgreSQL schreibt excluded konventionell groß und akzeptiert beides. Es erlaubt außerdem ein WHERE am Update-Zweig, sodass Sie nur aktualisieren, wenn eine Bedingung zutrifft.

MySQL und 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 — steht hier zum Vergleich. Die Beispieldatenbank im Browser ist SQLite, deshalb lässt sich diese Anweisung hier nicht ausführen.

Die Klausel unterscheidet sich um mehr als den Namen. Sie reagiert darauf, dass irgendeine Unique-Bedingung verletzt wird, nicht eine von Ihnen benannte; auf einer Tabelle mit mehreren Unique-Schlüsseln aktualisiert die Anweisung also über den, der tatsächlich kollidiert ist – und das ist nicht immer der, den Sie im Sinn hatten. MySQL 8.0.20 hat VALUES(col) zugunsten eines Alias für die neue Zeile abgekündigt, neuer Code sollte also die Form AS new verwenden; MariaDB will weiterhin VALUES().

SQL Server und 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 — steht hier zum Vergleich. Die Beispieldatenbank im Browser ist SQLite, deshalb lässt sich diese Anweisung hier nicht ausführen.

MERGE ist die Antwort des SQL-Standards und mit Abstand die geschwätzigste: Die eingehende Zeile wird als Abfrage geschrieben, mit dem Ziel gejoint, und jeder Zweig ausbuchstabiert. Dafür ist sie allgemeiner – WHEN NOT MATCHED BY SOURCE THEN DELETE macht daraus eine vollständige Synchronisation. Oracle braucht innerhalb von PL/SQL kein abschließendes Semikolon, SQL Server verlangt eines, und das ist der übliche Grund, warum ein kopiertes MERGE beim ersten Versuch scheitert.

Für eine einzelne Zeile machen auf SQL Server viele Teams stattdessen ein UPDATE gefolgt von einem durch @@ROWCOUNT abgesicherten INSERT innerhalb einer Transaktion, weil zu MERGE eine lange Liste gemeldeter Fehler rund um Nebenläufigkeit und Trigger existiert. Wenn Sie es dort einsetzen, halten Sie es in einer Transaktion mit der richtigen Isolationsstufe und betrachten Sie es nicht für sich allein als atomar.

REPLACE ist kein Upsert

SQLite und MySQL haben beide REPLACE, und es sieht nach der kurzen Variante aus. Es ist nicht dieselbe Operation.

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);
In Planche ausführen

1 Zeile geändert

Führen Sie die Prüfabfrage erneut aus, und added_at ist nicht mehr das Datum aus 2026 – es ist die aktuelle Zeit. REPLACE löscht die kollidierende Zeile und fügt eine neue ein, jede nicht aufgezählte Spalte fällt also auf ihren Standardwert zurück. Auf einer breiteren Tabelle verwirft das still echte Daten, und das Löschen löst Delete-Trigger aus und kaskadiert über Fremdschlüssel zu Kindzeilen.

Benutzen Sie es nur, wenn Sie wirklich meinen "wirf die alte Zeile weg". Für "einfügen oder aktualisieren" nehmen Sie ON CONFLICT.