Menu

SQLite UPSERT: ON CONFLICT DO UPDATE & DO NOTHING

UPSERT in SQLite verstehen: ON CONFLICT richtig einsetzen, die excluded-Pseudotabelle nutzen, DO NOTHING vs. DO UPDATE und der Unterschied zu INSERT OR REPLACE.

Diese Seite enthält ausführbare Editoren — bearbeiten, ausführen und Ausgabe sofort sehen.

Einfügen oder aktualisieren, falls schon vorhanden

Ein klassisches Szenario: Du willst eine Zeile einfügen, aber falls bereits eine Zeile mit demselben Schlüssel existiert, soll sie stattdessen aktualisiert werden. Ohne UPSERT müsstest du zuerst ein SELECT absetzen und dann je nach Ergebnis ein INSERT oder ein UPDATE ausführen – das bedeutet zwei Roundtrips und dazwischen eine schöne Race Condition.

Mit dem SQLite UPSERT erledigst du das in einem einzigen Statement:

Beim ersten Ausführen wird die Zeile eingefügt. Startest du es noch einmal mit einem anderen Preis, aber derselben sku, wird der vorhandene Datensatz an Ort und Stelle aktualisiert – kein Duplikat, kein Fehler.

Aufbau von ON CONFLICT im Detail

So sieht die vollständige Syntax aus:

INSERT INTO table (...) VALUES (...)
ON CONFLICT(conflict_target) DO UPDATE SET col = expr, ...
WHERE condition;

Drei Bausteine sind hier entscheidend:

  • conflict_target — die Spalte (oder Spalten) mit einem UNIQUE- oder PRIMARY KEY-Constraint, bei der du eine Kollision erwartest. SQLite nutzt das, um den passenden Index zu überwachen.
  • DO UPDATE SET ... — was an der bestehenden Zeile geändert werden soll, wenn es zu einer Kollision kommt. (Oder DO NOTHING, um sie still zu überspringen.)
  • Optionales WHERE — eine zusätzliche Bedingung, die erfüllt sein muss, damit das Update tatsächlich ausgeführt wird.

Das Conflict-Target muss zu einem tatsächlich existierenden Unique-Constraint passen. ON CONFLICT(price) lässt sich nicht kompilieren, wenn price nicht unique ist — SQLite hat dann nichts, woran es einen Konflikt erkennen könnte.

DO NOTHING: Einfügen, falls nicht vorhanden – sonst überspringen

Die einfachere Variante. Praktisch, wenn du Daten initial befüllst (Seeding) oder Events protokollierst und Duplikate einfach stillschweigend ignoriert werden sollen:

Der zweite Insert trifft auf dieselbe event_id und würde normalerweise mit UNIQUE constraint failed abbrechen. Dank DO NOTHING überspringt SQLite die Zeile einfach – ohne Exception, ohne betroffene Zeile.

Genau das ist der typische "idempotente Insert", für den viele zu INSERT OR IGNORE greifen. Das DO NOTHING aus dem UPSERT erledigt denselben Job, lässt sich aber deutlich besser mit WHERE und RETURNING kombinieren.

Die excluded-Pseudotabelle in SQLite

Sobald ein Konflikt auftritt, hast du plötzlich zwei Zeilen im Spiel: die bestehende Zeile in der Tabelle und die neue, die du einfügen wolltest. SQLite stellt dir eine elegante Möglichkeit zur Verfügung, beide anzusprechen.

  • Reine Spaltennamen (price, name) verweisen auf die bestehende Zeile.
  • excluded.column verweist auf die neue, abgewiesene Zeile.

quantity = quantity + excluded.quantity liest sich wie "die bisherige Menge plus die neue". Nach zwei Inserts steht A-100 bei 8. Genau dieses Muster — Werte in eine vorhandene Zeile aufsummieren — ist einer der nützlichsten Kniffe beim UPSERT in SQLite.

Bedingtes UPSERT mit WHERE

Mit einem nachgestellten WHERE lässt sich das Update überspringen, sofern eine Bedingung nicht zutrifft. Die Bedingung wird gegen die bestehende Zeile geprüft und kann über excluded.* auch auf die neuen Werte zugreifen:

Die neue Zeile bringt ein älteres updated_at mit, also ist die WHERE-Bedingung falsch und das Update wird übersprungen. Die bestehende Zeile behält ihren neueren Preis. Dreht man die Datumswerte um, läuft das Update durch. Das ist das klassische Muster nach dem Motto „nur mit frischeren Daten überschreiben".

Mehrere Zeilen per Upsert einfügen

In VALUES lassen sich beliebig viele Zeilen unterbringen, und ON CONFLICT greift für jede einzelne davon unabhängig:

A-100 kollidiert und wird aktualisiert. A-200 und A-300 sind neu und werden eingefügt. Ein einziges Statement, gemischtes Ergebnis aus Insert und Update. Eine saubere Methode, um einen Schwung Datensätze aus einer externen Quelle zu synchronisieren.

UPSERT vs. INSERT OR REPLACE

INSERT OR REPLACE sieht auf den ersten Blick so aus, als würde es dasselbe tun. Tut es aber nicht.

notes ist weg. INSERT OR REPLACE hat Zeile 1 komplett gelöscht und eine neue eingefügt — alle Spalten, die du nicht angegeben hast, wurden auf NULL oder den Default-Wert zurückgesetzt. Außerdem werden dabei DELETE-Trigger ausgelöst und ON DELETE-Fremdschlüssel kaskadieren.

Ein echtes UPSERT lässt die Zeile dagegen bestehen:

notes ist weiterhin vorhanden. Geändert wurden nur die Spalten, die im SET aufgeführt sind. Greife standardmäßig zu UPSERT – INSERT OR REPLACE solltest du wirklich nur dann verwenden, wenn du tatsächlich die Semantik „löschen und neu einfügen" brauchst.

Mehrere Konfliktziele

Falls eine Zeile mit mehr als einem Constraint kollidieren könnte, kannst du mehrere ON CONFLICT-Klauseln aneinanderreihen:

Welche Constraint zuerst zündet, gewinnt – und der dazugehörige DO UPDATE-Zweig wird ausgeführt. In der Praxis hat die meiste Tabelle ohnehin nur ein offensichtliches Conflict-Target: den Primärschlüssel oder eine einzelne Unique-Spalte. Mehr als eine Klausel brauchst du selten.

Typische Stolperfallen

Ein paar Dinge, über die viele stolpern:

  • Ohne passenden Unique-Index kein UPSERT. ON CONFLICT(col) setzt voraus, dass col ein PRIMARY KEY ist oder eine UNIQUE-Constraint hat. Andernfalls bricht SQLite mit "no such constraint" ab.
  • DO UPDATE läuft nur bei einem tatsächlichen Konflikt. Es ist eine Alternative zum Insert, kein Zusatzverhalten. Wenn der Schlüssel zum ersten Mal auftaucht, wird ausschließlich eingefügt.
  • excluded ist nur lesbar. Du kannst daraus lesen, aber nichts hineinschreiben. Ziel von SET ist immer die bereits vorhandene Zeile.
  • Automatisch vergebene INTEGER PRIMARY KEY-Rowids. Lieferst du die ID nicht selbst, bekommt jedes Insert eine neue – es gibt also gar nichts, womit ein Konflikt entstehen könnte. UPSERT ergibt nur Sinn, wenn die Konfliktspalte einen deterministischen Wert hat, den der Aufrufer mitgibt.

Als Nächstes: RETURNING

UPSERT verrät dir nicht, welche Zeilen eingefügt und welche aktualisiert wurden – und schon gar nicht, wie sie am Ende aussehen. Genau dafür gibt es die RETURNING-Klausel: Sie gibt dir die betroffenen Zeilen direkt im selben Statement zurück, ganz ohne nachgelagertes SELECT. Darum geht es im nächsten Abschnitt.

Häufig gestellte Fragen

Was ist UPSERT in SQLite überhaupt?

UPSERT ist ein INSERT, der zu einem UPDATE (oder einem No-op) wird, sobald er sonst gegen einen UNIQUE- oder PRIMARY KEY-Constraint verstoßen würde. Geschrieben wird das als INSERT ... ON CONFLICT(spalte) DO UPDATE SET ... oder DO NOTHING. SQLite kann das seit Version 3.24.0 (2018).

Was hat es mit der excluded-Tabelle beim UPSERT auf sich?

excluded ist eine spezielle Pseudotabelle, in der die Zeile steckt, die du eigentlich einfügen wolltest. Innerhalb von DO UPDATE SET ... sprichst du die bestehende Zeile über den Spaltennamen an und die abgelehnte Zeile über excluded.spalte. SET price = excluded.price heißt also schlicht: 'Übernimm den price-Wert aus dem neuen INSERT'.

Wo liegt der Unterschied zwischen INSERT OR REPLACE und UPSERT?

INSERT OR REPLACE löscht die kollidierende Zeile und legt eine frische an – das feuert DELETE-Trigger, reißt Foreign Keys mit ON DELETE CASCADE mit und setzt alle Spalten auf ihre Defaults zurück. UPSERT dagegen aktualisiert die bestehende Zeile direkt; geändert wird nur, was du im SET aufzählst. Nimm UPSERT, außer du willst wirklich löschen-und-neu-einfügen.

Kann ich in SQLite mehrere Zeilen auf einmal upserten?

Ja. INSERT INTO t(...) VALUES (...), (...), (...) ON CONFLICT(spalte) DO UPDATE SET ... läuft problemlos. Jede Zeile wird einzeln gegen das Conflict-Target geprüft, und excluded innerhalb von DO UPDATE zeigt auf genau die eingehende Zeile, die den Konflikt ausgelöst hat.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S