Abschnitt 14 · Lektion 5
Ändern und löschen
INSERT, UPDATE und DELETE sind die drei Befehle, mit denen sich in einer Tabelle etwas ändert.
Sie funktionieren alle gleich: Du bereitest sie mit prepare() vor, gibst die Werte an execute()
mit, und danach fragst du nach, was passiert ist. Die Regel aus der letzten Lektion gilt hier genauso
und ohne Ausnahme.
Die drei sehen so aus: INSERT INTO notizen (titel) VALUES (:titel) legt an,
UPDATE notizen SET erledigt = 1 WHERE id = :id ändert, und
DELETE FROM notizen WHERE id = :id entfernt.
Was du nach dem Ausführen wissen willst, ist bei den drei verschieden. Beim INSERT willst du die
Nummer der neuen Zeile. Bei UPDATE und DELETE willst du wissen, ob überhaupt etwas getroffen
wurde.
Die Nummer der neuen Zeile
<?php
// Drei Notizen, alle offen: 1 Milch, 2 Brot, 3 Kaffee. Die Tabelle
// wird bei jedem Lauf neu aufgebaut, an dieser Datei aenderst du nichts.
$db = new PDO("sqlite:" . __DIR__ . "/daten.db", null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$db->exec("DROP TABLE IF EXISTS notizen");
$db->exec("CREATE TABLE notizen (
id INTEGER PRIMARY KEY,
titel TEXT NOT NULL,
erledigt INTEGER NOT NULL DEFAULT 0
)");
$einfuegen = $db->prepare("INSERT INTO notizen (titel) VALUES (?)");
foreach (["Milch", "Brot", "Kaffee"] as $titel) {
$einfuegen->execute([$titel]);
} <?php
// Einfuegen, und die Nummer, die dabei herauskommt.
require __DIR__ . "/vorbereiten.php";
$einfuegen = $db->prepare("INSERT INTO notizen (titel) VALUES (:titel)");
$einfuegen->execute([":titel" => "Zucker"]);
echo "Die neue Nummer ist: ";
var_dump($db->lastInsertId());
echo "Als Zahl gebraucht, also mit (int) davor: ";
var_dump((int) $db->lastInsertId());
$einfuegen->execute([":titel" => "Mehl"]);
$einfuegen->execute([":titel" => "Salz"]);
echo "\nNach zwei weiteren steht dort die letzte, nicht die erste:\n";
echo " ", $db->lastInsertId(), "\n";
echo "\nUnd nach einem UPDATE steht dort immer noch dieselbe Zahl:\n";
$db->prepare("UPDATE notizen SET erledigt = 1 WHERE id = :id")->execute([":id" => 1]);
echo " ", $db->lastInsertId(), "\n"; $db->lastInsertId() gibt die Nummer der zuletzt eingefügten Zeile zurück. Du brauchst sie öfter,
als du denkst: um gleich danach auf die neue Notiz weiterzuleiten, um sie in einer zweiten Tabelle
zu verknüpfen, oder um sie dem Benutzer zu nennen.
Zwei Dinge daran solltest du wissen. Erstens ist der Rückgabewert eine Zeichenkette und keine
Zahl, das siehst du im var_dump als string(1) "4". Das liegt daran, dass eine Datenbank auch
Schlüssel vergeben kann, die in kein PHP-Integer passen. Brauchst du wirklich eine Zahl, schreibst
du (int) davor.
Ganz ehrlich: Zwingend nötig ist das (int) meistens nicht. Eine Funktion mit dem Rückgabetyp int
nimmt die Zeichenkette "4" klaglos an und macht selbst eine 4 daraus, weil PHP ohne
declare(strict_types=1) zwischen Zahl und Zahlentext hin und her wandelt. Mit strict_types
dagegen ist genau das ein TypeError. Schreib das (int) trotzdem hin: Es kostet nichts und macht
sichtbar, dass hier eine Umwandlung passiert, statt sie PHP zu überlassen.
Zweitens heißt „zuletzt” wirklich zuletzt. Fügst du drei Zeilen ein, bekommst du danach die Nummer
der dritten. Willst du alle drei Nummern, holst du sie jeweils direkt nach ihrem execute() ab.
Ein UPDATE dazwischen ändert den Wert übrigens nicht, dort wird ja nichts eingefügt.
Hat das etwas getroffen?
<?php
// Drei Notizen, alle offen: 1 Milch, 2 Brot, 3 Kaffee. Die Tabelle
// wird bei jedem Lauf neu aufgebaut, an dieser Datei aenderst du nichts.
$db = new PDO("sqlite:" . __DIR__ . "/daten.db", null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$db->exec("DROP TABLE IF EXISTS notizen");
$db->exec("CREATE TABLE notizen (
id INTEGER PRIMARY KEY,
titel TEXT NOT NULL,
erledigt INTEGER NOT NULL DEFAULT 0
)");
$einfuegen = $db->prepare("INSERT INTO notizen (titel) VALUES (?)");
foreach (["Milch", "Brot", "Kaffee"] as $titel) {
$einfuegen->execute([$titel]);
} <?php
// Wie viele Zeilen hat das getroffen?
require __DIR__ . "/vorbereiten.php";
$erledigen = $db->prepare("UPDATE notizen SET erledigt = 1 WHERE id = :id");
$erledigen->execute([":id" => 2]);
echo "UPDATE auf Notiz 2: ", $erledigen->rowCount(), "\n";
$erledigen->execute([":id" => 2]);
echo "Noch einmal dieselbe: ", $erledigen->rowCount(), "\n";
$erledigen->execute([":id" => 99]);
echo "UPDATE auf Notiz 99: ", $erledigen->rowCount(), "\n";
$loeschen = $db->prepare("DELETE FROM notizen WHERE id = :id");
$loeschen->execute([":id" => 3]);
echo "DELETE auf Notiz 3: ", $loeschen->rowCount(), "\n";
$alle = $db->query("SELECT * FROM notizen");
echo "\nUnd bei einem SELECT:\n";
echo " rowCount() sagt: ", $alle->rowCount(), "\n";
echo " wirklich da sind: ", count($alle->fetchAll()), "\n"; $stmt->rowCount() beantwortet nach einem UPDATE oder DELETE die Frage, wie viele Zeilen
betroffen waren. Meistens ist die Antwort 1, und interessant ist der Fall, in dem sie 0 ist: Dann
gab es die Zeile nicht, und dein Programm sollte etwas anderes tun, als so zu tun, als hätte es
geklappt.
Achte auf die zweite Zeile im Beispiel. Dieselbe Notiz wird zweimal auf erledigt gesetzt, und beim
zweiten Mal steht dort trotzdem 1. rowCount() zählt die Zeilen, auf die das WHERE gepasst hat,
nicht die Zeilen, in denen sich wirklich ein Wert geändert hat. Das ist meistens die Auskunft, die
man will, nur eben nicht die, die man erwartet.
Und ein Sonderfall, der Zeit kostet, wenn man ihn nicht kennt: Nach einem SELECT ist
rowCount() bei SQLite nicht zu gebrauchen. Im Beispiel sagt es 0, obwohl zwei Zeilen da sind. Die
Zahl der Treffer bekommst du entweder mit count($stmt->fetchAll()) oder, wenn du die Zeilen gar
nicht brauchst, mit einer eigenen Abfrage SELECT count(*) FROM ....
Das vergessene WHERE
<?php
// Drei Notizen, alle offen: 1 Milch, 2 Brot, 3 Kaffee. Die Tabelle
// wird bei jedem Lauf neu aufgebaut, an dieser Datei aenderst du nichts.
$db = new PDO("sqlite:" . __DIR__ . "/daten.db", null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$db->exec("DROP TABLE IF EXISTS notizen");
$db->exec("CREATE TABLE notizen (
id INTEGER PRIMARY KEY,
titel TEXT NOT NULL,
erledigt INTEGER NOT NULL DEFAULT 0
)");
$einfuegen = $db->prepare("INSERT INTO notizen (titel) VALUES (?)");
foreach (["Milch", "Brot", "Kaffee"] as $titel) {
$einfuegen->execute([$titel]);
} <?php
// Ein vergessenes WHERE. Sieh dir die zweite Tabelle an.
require __DIR__ . "/vorbereiten.php";
function zeigen(PDO $db, string $wann): void
{
echo $wann, "\n";
foreach ($db->query("SELECT id, titel, erledigt FROM notizen ORDER BY id") as $zeile) {
echo " ", $zeile["id"], " ", str_pad($zeile["titel"], 8), "erledigt=", $zeile["erledigt"], "\n";
}
echo "\n";
}
zeigen($db, "Vorher:");
$db->exec("UPDATE notizen SET erledigt = 1");
zeigen($db, "Nach UPDATE notizen SET erledigt = 1 ohne WHERE:"); UPDATE notizen SET erledigt = 1 ohne WHERE ist eine vollkommen gültige Anweisung. Sie setzt
erledigt in jeder Zeile auf 1. SQLite fragt nicht nach, es gibt keine Warnung, und rückgängig
machen kannst du es nicht. Dasselbe gilt für DELETE FROM notizen, nur ist danach die ganze Tabelle
leer.
Das ist kein hypothetischer Fehler. Er passiert, wenn man eine Abfrage in einem Datenbankwerkzeug
zusammenbaut und zu früh auf Ausführen klickt, und er passiert im Code, wenn eine Variable leer ist
und das WHERE aus einer Bedingung zusammengesetzt wurde.
Zwei Gewohnheiten helfen wirklich. Schreib das WHERE zuerst und den SET-Teil danach, dann
kann es nicht fehlen. Und probier eine Änderung, bei der du unsicher bist, erst als SELECT mit
demselben WHERE aus: Was der SELECT anzeigt, ist genau das, was das UPDATE treffen wird.
Löschen, oder lieber doch nicht
<?php
// Drei Notizen, alle offen: 1 Milch, 2 Brot, 3 Kaffee. Die Tabelle
// wird bei jedem Lauf neu aufgebaut, an dieser Datei aenderst du nichts.
$db = new PDO("sqlite:" . __DIR__ . "/daten.db", null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$db->exec("DROP TABLE IF EXISTS notizen");
$db->exec("CREATE TABLE notizen (
id INTEGER PRIMARY KEY,
titel TEXT NOT NULL,
erledigt INTEGER NOT NULL DEFAULT 0
)");
$einfuegen = $db->prepare("INSERT INTO notizen (titel) VALUES (?)");
foreach (["Milch", "Brot", "Kaffee"] as $titel) {
$einfuegen->execute([$titel]);
} <?php
// Wirklich loeschen, oder nur als geloescht markieren.
require __DIR__ . "/vorbereiten.php";
$db->exec("ALTER TABLE notizen ADD COLUMN geloescht_am TEXT");
echo "Wirklich loeschen. Danach ist die Zeile weg:\n";
$db->prepare("DELETE FROM notizen WHERE id = ?")->execute([1]);
echo " Zeilen in der Tabelle: ", $db->query("SELECT count(*) FROM notizen")->fetchColumn(), "\n";
echo " Notiz 1 noch da: ";
var_dump($db->query("SELECT * FROM notizen WHERE id = 1")->fetch());
echo "\nAls geloescht markieren. Die Zeile bleibt, die Liste wird kuerzer:\n";
$db->prepare("UPDATE notizen SET geloescht_am = ? WHERE id = ?")->execute(["2026-08-29", 2]);
echo " Zeilen in der Tabelle: ", $db->query("SELECT count(*) FROM notizen")->fetchColumn(), "\n";
echo " Zeilen in der Liste: ", $db->query("SELECT count(*) FROM notizen WHERE geloescht_am IS NULL")->fetchColumn(), "\n";
echo " Notiz 2 sagt: geloescht am ", $db->query("SELECT geloescht_am FROM notizen WHERE id = 2")->fetchColumn(), "\n"; DELETE entfernt die Zeile, und danach ist sie weg. Das klingt richtig und ist es oft auch. In einer
Anwendung, die Menschen benutzen, ist es das öfter nicht: Jemand löscht versehentlich, jemand will
wissen, was letzte Woche in der Liste stand, oder eine zweite Tabelle verweist noch auf die Zeile.
Der übliche Ausweg heißt Soft Delete und ist einfacher, als der Name klingt. Statt zu löschen, setzt
du eine Spalte, etwa geloescht_am. Die Zeile bleibt liegen, und überall dort, wo du die Liste
anzeigst, ergänzt du WHERE geloescht_am IS NULL.
Bezahlt wird das damit, dass du diese Bedingung nirgends vergessen darfst, sonst tauchen gelöschte
Einträge wieder auf. Für die Notizliste in diesem Kurs bleiben wir deshalb beim echten DELETE. Für
Rechnungen, Bestellungen und alles, was jemand nachvollziehen können muss, ist die Spalte die
bessere Wahl.
IS NULL ist dabei kein Tippfehler: In SQL prüfst du auf einen leeren Wert mit IS NULL und nicht
mit = NULL. Ein Vergleich mit NULL ergibt nämlich weder wahr noch falsch, sondern wieder NULL,
und damit trifft WHERE spalte = NULL grundsätzlich keine einzige Zeile.
Zum Mitnehmen
Drei Befehle, und einer davon hat einen Ruf. Ein UPDATE ohne WHERE ändert nicht eine Zeile, sondern alle, und es fragt vorher nicht nach. Das ist keine Anekdote, das passiert regelmäßig.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 4 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.