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:43Der 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:
- 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 BYgibt es hier nicht. Wenn du mehrere Zeilen zurückbekommst und die Reihenfolge brauchst, sortiere in der Anwendung. - Kein Einsatz als Unterabfrage.
RETURNINGliefert Daten ausschließlich an die Anwendung – du kannst das Statement nicht in ein anderesSELECTschachteln. - 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. - 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
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 →Verwandte Snippets
WITH RECURSIVE – Kategoriebäume und Kommentar-Threads in einer einzigen Abfrage
Kategorien mit Unterkategorien, Kommentare mit Antworten, Ordner in Ordnern: Die meisten holen sich so etwas mit einer Schleife und einer Abfrage pro Ebene. Eine rekursive CTE holt den ganzen Baum in einem Rutsch – inklusive Tiefe und Pfad.
CREATE VIEW – die Abfrage, die du nur einmal schreibst
Wenn dieselbe 15-zeilige Abfrage an vier Stellen im Code steht, ändert man sie irgendwann an dreien. Eine View gibt ihr einen Namen – danach steht sie nur noch an einer Stelle.
Volltextsuche in SQLite mit FTS5 – schneller und besser als LIKE '%wort%
LIKE '%suchwort%' kann keinen Index nutzen und liest bei jeder Suche die ganze Tabelle. FTS5 ist in SQLite eingebaut, findet auch Wortanfänge und sortiert die Treffer nach Relevanz – ohne Elasticsearch daneben.