MWCodebymw.de ↗
SQL

RETURNING in SQLite – die geschriebene Zeile sofort zurückbekommen

Nach einem INSERT oder UPDATE noch einmal SELECTen, um zu sehen, was drinsteht? Muss nicht sein. RETURNING liefert die betroffenen Zeilen direkt aus dem schreibenden Statement – ohne zweite Abfrage und ohne die Lücke dazwischen.

Das Muster kennst du: Du fügst eine Zeile ein und brauchst danach die ID. Oder du zählst ein Guthaben hoch und willst wissen, wie hoch es jetzt ist. Der Reflex ist ein zweites Statement – lastInsertId() oder ein hinterhergeschobenes SELECT.

Seit SQLite 3.35.0 (März 2021) geht das in einem Rutsch. RETURNING hängt sich an INSERT, UPDATE und DELETE und gibt dir die betroffenen Zeilen zurück, als wäre das Statement ein SELECT gewesen.

Der einfache Fall: INSERT

CREATE TABLE kunden (
    id        INTEGER PRIMARY KEY,
    name      TEXT NOT NULL,
    guthaben  INTEGER NOT NULL DEFAULT 0,
    angelegt  TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO kunden (name) VALUES ('Meier GmbH')
RETURNING id, angelegt;

Ergebnis:

id  angelegt
1   2026-08-05 10:47:43

Der Gewinn ist nicht die ID – die bekommst du auch über lastInsertId(). Der Gewinn sind angelegt und alles andere, was die Datenbank gesetzt hat: Default-Werte, berechnete Spalten, ein Zeitstempel aus CURRENT_TIMESTAMP. Das musst du sonst nachlesen, und dazu brauchst du erst die ID, um die Zeile zu finden.

Der Fall, für den es keine Alternative gibt: UPDATE

Für INSERT gibt es lastInsertId(). Für UPDATE gibt es nichts Vergleichbares – da bleibt nur ein zweites SELECT, und zwischen den beiden Statements kann die Zeile sich schon wieder geändert haben.

UPDATE kunden
   SET guthaben = guthaben + 250
 WHERE id = 1
RETURNING guthaben;

Das liefert 250 – den Wert nach dem Schreiben, aus demselben Statement. Kein Zeitfenster, in dem jemand anderes dazwischenfunkt.

Dasselbe beim Aufräumen: DELETE ... RETURNING sagt dir, was du gerade weggeworfen hast.

DELETE FROM kunden WHERE guthaben < 1000
RETURNING id, name;

Praktisch, wenn du das Ergebnis protokollieren willst. Vorher hättest du erst selektieren und dann löschen müssen – zwei Statements, zwischen denen sich die Menge ändern kann.

In PHP mit PDO

Ein schreibendes Statement mit RETURNING liefert Zeilen. Du holst sie ganz normal mit fetch() ab:

$st = $db->prepare(
    'INSERT INTO kunden (name) VALUES (?) RETURNING id, angelegt'
);
$st->execute(['Meier GmbH']);
$zeile = $st->fetch(PDO::FETCH_ASSOC);
// ['id' => 1, 'angelegt' => '2026-08-05 10:47:43']

Und ein Nebeneffekt, den ich sehr mag: Bei einem UPDATE, das keine Zeile trifft, gibt fetch() einfach false zurück.

$st = $db->prepare('UPDATE kunden SET guthaben = ? WHERE id = ? RETURNING guthaben');
$st->execute([500, 999]);

if (($neu = $st->fetchColumn()) === false) {
    // Kunde gibt es nicht – sauber unterscheidbar von „Guthaben ist 0"
}

Damit beantwortet ein einziges Statement zwei Fragen auf einmal: *Hat es überhaupt eine Zeile getroffen?* und *Was steht jetzt drin?* Über rowCount() bekommst du nur die erste Hälfte.

Vier Fallstricke, die in der Doku stehen

Das ist kein Allheilmittel, und die Einschränkungen sind unangenehm konkret:

  1. Die Reihenfolge der Zeilen ist beliebig. Die SQLite-Doku ist da deutlich: Die Reihenfolge kann sich zwischen Versionen und sogar zwischen zwei Ausführungen desselben Statements ändern, und es gibt keine Möglichkeit, sie festzulegen. Ein ORDER BY gibt es hier nicht. Wenn du mehrere Zeilen zurückbekommst und die Reihenfolge brauchst, sortiere in der Anwendung.
  2. Kein Einsatz als Unterabfrage. RETURNING liefert Daten ausschließlich an die Anwendung – du kannst das Statement nicht in ein anderes SELECT schachteln.
  3. Trigger-Änderungen sind nicht drin. Zurückgegeben werden die Werte, wie das Statement selbst sie sieht. Was ein AFTER-Trigger danach noch umschreibt, steht nicht in der Antwort.
  4. Nicht in Triggern, nicht bei virtuellen Tabellen (bei DELETE/UPDATE) – also zum Beispiel nicht auf einer FTS5-Tabelle.

Und noch etwas Praktisches: Deine gelinkte SQLite-Version muss mindestens 3.35 sein. Bei PHP prüfst du das in einer Zeile:

SELECT sqlite_version();

Auf meinem Rechner steht dort 3.45 – das ist bei aktuellen PHP-Versionen der Normalfall. Nur bei sehr alten Shared-Hostern lohnt der Blick.

Wo ich das benutze

In meinem eigenen CMS hängt an fast jedem Schreibvorgang eine Nachfrage: Wie lautet die neue Version? Wann wurde gespeichert? Wie hoch ist der Zähler jetzt? Früher waren das zwei Statements, heute eines. Das ist kein Performance-Trick – bei SQLite sind beide Wege schnell –, sondern eine Frage der Sicherheit: Zwischen zwei Statements kann etwas passieren, innerhalb eines nicht.

Wenn du eine Website mit eigenem CMS oder eine Android-App brauchst, bei der die Datenschicht sauber aufgebaut ist statt „läuft irgendwie": Schreib mir über bymw.de.

Quellen

#RETURNING#SQLite#INSERT#UPDATE#PDO

Du brauchst mehr als ein Snippet?

Ich entwickle Android-Apps in Kotlin und moderne Websites für Selbstständige und kleine Unternehmen — von der ersten Idee bis zum Release.

Projekt anfragen →