Abschnitt 14 · Lektion 1
Warum eine Datenbank
Seit Abschnitt 8 legst du Daten in Dateien ab, und seit Abschnitt 11 wächst eine Notizliste in einer JSON-Datei. Das hat gut funktioniert, und es funktioniert auch weiterhin gut, solange die Liste klein bleibt und immer nur einer daran arbeitet.
Wie Abschnitt 13 spielt auch dieser auf der Kommandozeile: Was du siehst, steht im Terminal, und der Browser bleibt leer, bis die Liste in den Lektionen 14.7 und 14.8 wieder eine Seite wird.
Es gibt drei Stellen, an denen es aufhört zu funktionieren. Keine davon ist eine Geschmacksfrage.
Erstens: zwei, die gleichzeitig schreiben
Den Fall hast du in Lektion 11.5 schon gesehen. Zwei Anfragen kommen fast gleichzeitig an, beide lesen dieselbe Datei, beide hängen ihre Notiz an das gelesene Array an, beide schreiben zurück. Wer zuletzt schreibt, gewinnt, und die Notiz des anderen ist weg. Nicht kaputt, nicht halb da, einfach weg, und niemand bekommt eine Fehlermeldung.
Auf deinem Rechner passiert das nie, weil du allein bist. Auf einem Server mit dreißig Besuchern
passiert es an einem ruhigen Nachmittag mehrmals. Du kannst dagegen mit flock() eine Sperre bauen,
und dann hast du angefangen, eine Datenbank nachzubauen.
Zweitens: suchen
[
{ "titel": "Milch", "verfasser": "Mia", "erledigt": false, "angelegt": "2026-08-24" },
{ "titel": "Brot", "verfasser": "Ben", "erledigt": false, "angelegt": "2026-08-23" },
{ "titel": "Kaffee", "verfasser": "Mia", "erledigt": true, "angelegt": "2026-08-22" },
{ "titel": "Zahnpasta", "verfasser": "Mia", "erledigt": false, "angelegt": "2026-08-21" },
{ "titel": "Batterien", "verfasser": "Ben", "erledigt": true, "angelegt": "2026-08-20" },
{ "titel": "Kerzen", "verfasser": "Mia", "erledigt": false, "angelegt": "2026-08-19" },
{ "titel": "Seife", "verfasser": "Mia", "erledigt": false, "angelegt": "2026-08-18" },
{ "titel": "Tee", "verfasser": "Ben", "erledigt": false, "angelegt": "2026-08-17" }
] <?php
// Die Aufgabe: die drei neuesten offenen Notizen von Mia.
// Mit einer Datei musst du jeden Schritt selbst machen.
$notizen = json_decode(file_get_contents(__DIR__ . "/notizen.json"), true);
$treffer = [];
foreach ($notizen as $notiz) {
if ($notiz["verfasser"] === "Mia" && $notiz["erledigt"] === false) {
$treffer[] = $notiz;
}
}
usort($treffer, fn(array $a, array $b) => strcmp($b["angelegt"], $a["angelegt"]));
$treffer = array_slice($treffer, 0, 3);
foreach ($treffer as $notiz) {
echo $notiz["angelegt"], " ", $notiz["titel"], "\n";
} Die Frage ist harmlos: die drei neuesten offenen Notizen von Mia. Sieh dir an, was dafür passieren
muss. Erst wird die ganze Datei gelesen und in ein Array verwandelt, dann wird gefiltert, dann
sortiert, dann abgeschnitten. Vier Schritte, und drei davon musst du selbst schreiben. Im Reiter
Debug siehst du die Arbeit: treffer wächst in der Schleife auf vier Einträge, und erst
array_slice macht drei daraus.
Schlimmer als die Zeilenzahl ist das erste Wort in dem Absatz: die ganze Datei. Bei acht Notizen merkst du nichts. Bei zwanzigtausend liest dein Programm zwanzigtausend Einträge in den Speicher, um drei davon zu behalten, und zwar bei jedem einzelnen Seitenaufruf.
[
{ "titel": "Milch", "verfasser": "Mia", "erledigt": false, "angelegt": "2026-08-24" },
{ "titel": "Brot", "verfasser": "Ben", "erledigt": false, "angelegt": "2026-08-23" },
{ "titel": "Kaffee", "verfasser": "Mia", "erledigt": true, "angelegt": "2026-08-22" },
{ "titel": "Zahnpasta", "verfasser": "Mia", "erledigt": false, "angelegt": "2026-08-21" },
{ "titel": "Batterien", "verfasser": "Ben", "erledigt": true, "angelegt": "2026-08-20" },
{ "titel": "Kerzen", "verfasser": "Mia", "erledigt": false, "angelegt": "2026-08-19" },
{ "titel": "Seife", "verfasser": "Mia", "erledigt": false, "angelegt": "2026-08-18" },
{ "titel": "Tee", "verfasser": "Ben", "erledigt": false, "angelegt": "2026-08-17" }
] <?php
// Diese Datei baut die Datenbank aus notizen.json auf, damit nebenan
// etwas zum Abfragen da ist. Wie das geht, steht in Lektion 14.2.
$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,
verfasser TEXT NOT NULL,
erledigt INTEGER NOT NULL DEFAULT 0,
angelegt TEXT NOT NULL
)");
$einfuegen = $db->prepare("INSERT INTO notizen (titel, verfasser, erledigt, angelegt)
VALUES (:titel, :verfasser, :erledigt, :angelegt)");
foreach (json_decode(file_get_contents(__DIR__ . "/notizen.json"), true) as $notiz) {
$einfuegen->execute([
":titel" => $notiz["titel"],
":verfasser" => $notiz["verfasser"],
":erledigt" => $notiz["erledigt"] ? 1 : 0,
":angelegt" => $notiz["angelegt"],
]);
} <?php
// Dieselbe Aufgabe, dieselben acht Notizen: die drei neuesten offenen
// von Mia. Diesmal steht die Frage in der Abfrage.
require __DIR__ . "/vorbereiten.php";
$zeilen = $db->query("SELECT angelegt, titel
FROM notizen
WHERE verfasser = 'Mia' AND erledigt = 0
ORDER BY angelegt DESC
LIMIT 3")->fetchAll();
foreach ($zeilen as $zeile) {
echo $zeile["angelegt"], " ", $zeile["titel"], "\n";
} Dasselbe Ergebnis, dieselben acht Notizen. Der Unterschied steckt in der Mitte: Statt vier Schritten steht dort eine Frage, und zwar in einer eigenen Sprache. Sie heißt SQL, und du kannst diese Zeilen schon jetzt lesen, obwohl du sie noch nie geschrieben hast.
SELECT angelegt, titel sagt, welche Spalten du willst. FROM notizen sagt, aus welcher Tabelle.
WHERE ist die Bedingung, ORDER BY ... DESC die Sortierung, LIMIT 3 die Begrenzung. Genau die
vier Schritte von oben, nur beschreibst du das Was und nicht das Wie.
Und darin liegt der eigentliche Gewinn: Weil die Datenbank das Wie selbst entscheidet, kann sie es gut machen. Sie kann sich eine sortierte Übersicht anlegen, einen sogenannten Index, und dann sind die drei Treffer auch unter zwanzigtausend Notizen sofort da, ohne dass sich an deiner Abfrage ein Zeichen ändert.
Drittens: Dinge, die zusammengehören
In der JSON-Datei steht bei jeder Notiz ein verfasser, und fünf der acht Einträge nennen dort
Mia. Wenn Mia heiratet und anders heißt, änderst du fünf Stellen. Vergisst du eine, hast du zwei
Mias, und keine Suche findet mehr beide.
Der Ausweg ist, Verfasser und Notizen getrennt zu führen und in der Notiz nur noch die Nummer des
Verfassers abzulegen. Mit Dateien wird das schnell mühsam: Du müsstest bei jeder Anzeige zwei
Dateien lesen und die passenden Einträge selbst zusammensuchen. Eine Datenbank tut genau das mit
einem Wort, und dieses Wort ist JOIN. Lektion 14.7 zeigt es.
Was eine Datenbank stattdessen tut
Sie ist ein Programm, das deine Daten verwaltet und dabei drei Zusagen macht, die eine Datei nicht machen kann. Sie sortiert gleichzeitige Zugriffe, sodass niemandem etwas verlorengeht. Sie beantwortet Fragen selbst, statt dir alles hinzustellen. Und sie achtet auf Regeln, die du einmal festlegst, etwa dass ein Titel nicht leer sein darf.
Bezahlt wird das mit einer zweiten Sprache im Programm. SQL sieht anfangs fremd aus, ist aber kleiner, als es aussieht: Mit einer Handvoll Wörter kommst du sehr weit, und die lernst du in diesem Abschnitt alle.
SQLite und MySQL, in drei Sätzen
SQLite ist eine Datei. Deine Datenbank liegt als daten.db neben deinem Programm, es läuft kein
zweites Programm daneben, und PHP bringt alles mit, was es dafür braucht. Kopieren, sichern und
weggeben kannst du sie wie jede andere Datei.
MySQL und PostgreSQL sind Dienste. Dort läuft ein eigener Serverprozess, dein Programm meldet sich mit Adresse, Benutzername und Passwort an, und mehrere Programme auf mehreren Rechnern können gleichzeitig darauf zugreifen. Das ist mehr Aufwand und irgendwann mehr Wert.
Umsteigen kostet dich fast nichts, und das ist der Grund, warum wir hier mit SQLite anfangen. Deine Abfragen bleiben, wie sie sind, und in PHP ändert sich genau eine Zeile: die, in der du dich verbindest. Der Rest dieses Abschnitts gilt für beide.
Vier Wörter, die du ab jetzt brauchst
[
{ "titel": "Milch", "verfasser": "Mia", "erledigt": false, "angelegt": "2026-08-24" },
{ "titel": "Brot", "verfasser": "Ben", "erledigt": false, "angelegt": "2026-08-23" },
{ "titel": "Kaffee", "verfasser": "Mia", "erledigt": true, "angelegt": "2026-08-22" },
{ "titel": "Zahnpasta", "verfasser": "Mia", "erledigt": false, "angelegt": "2026-08-21" },
{ "titel": "Batterien", "verfasser": "Ben", "erledigt": true, "angelegt": "2026-08-20" },
{ "titel": "Kerzen", "verfasser": "Mia", "erledigt": false, "angelegt": "2026-08-19" },
{ "titel": "Seife", "verfasser": "Mia", "erledigt": false, "angelegt": "2026-08-18" },
{ "titel": "Tee", "verfasser": "Ben", "erledigt": false, "angelegt": "2026-08-17" }
] <?php
// Dieselbe Vorbereitung wie im Beispiel davor.
$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,
verfasser TEXT NOT NULL,
erledigt INTEGER NOT NULL DEFAULT 0,
angelegt TEXT NOT NULL
)");
$einfuegen = $db->prepare("INSERT INTO notizen (titel, verfasser, erledigt, angelegt)
VALUES (:titel, :verfasser, :erledigt, :angelegt)");
foreach (json_decode(file_get_contents(__DIR__ . "/notizen.json"), true) as $notiz) {
$einfuegen->execute([
":titel" => $notiz["titel"],
":verfasser" => $notiz["verfasser"],
":erledigt" => $notiz["erledigt"] ? 1 : 0,
":angelegt" => $notiz["angelegt"],
]);
} <?php
// Vier Woerter, an einer echten Tabelle.
require __DIR__ . "/vorbereiten.php";
$spalten = $db->query("PRAGMA table_info(notizen)")->fetchAll();
echo "Die Tabelle heisst notizen und hat ", count($spalten), " Spalten:\n";
foreach ($spalten as $spalte) {
echo " ", str_pad($spalte["name"], 12), $spalte["type"];
echo $spalte["pk"] ? " <- Primaerschluessel" : "";
echo "\n";
}
echo "\nUnd das sind die ersten drei Zeilen:\n";
foreach ($db->query("SELECT * FROM notizen ORDER BY id LIMIT 3") as $zeile) {
echo " ", implode(" | ", $zeile), "\n";
}
echo "\nZwei Zeilen duerfen denselben Titel haben:\n";
$db->exec("INSERT INTO notizen (titel, verfasser, erledigt, angelegt)
VALUES ('Milch', 'Ben', 0, '2026-08-25')");
echo " Notizen mit dem Titel Milch: ";
echo $db->query("SELECT count(*) FROM notizen WHERE titel = 'Milch'")->fetchColumn(), "\n";
echo "\nDieselbe Nummer zweimal geht dagegen nicht:\n";
try {
$db->exec("INSERT INTO notizen (id, titel, verfasser, erledigt, angelegt)
VALUES (1, 'Noch mehr Milch', 'Ben', 0, '2026-08-25')");
echo " angenommen\n";
} catch (PDOException $e) {
echo " ", $e->getMessage(), "\n";
} Eine Tabelle ist das, was in der JSON-Datei die Liste war: alle Notizen zusammen. Eine Zeile
ist eine einzelne Notiz. Eine Spalte ist eine Eigenschaft, die jede Notiz hat, also titel oder
erledigt. Anders als beim Array liegt vorher fest, welche Spalten es gibt und was für Werte
hineindürfen, und du kannst dir das mit PRAGMA table_info jederzeit ansehen.
Das vierte Wort ist das wichtigste. Der Primärschlüssel ist die Spalte, die eine Zeile eindeutig
macht, hier id. Zwei Notizen dürfen denselben Titel tragen, das Beispiel legt eine zweite Milch an
und niemand beschwert sich. Dieselbe Nummer zweimal geht nicht, und das siehst du in der letzten
Zeile der Ausgabe: UNIQUE constraint failed: notizen.id.
Genau das hat dir in der JSON-Datei gefehlt. Dort war eine Notiz „die dritte von oben”, und sobald jemand die zweite löscht, ist sie die zweite. Mit einer Nummer, die zu ihr gehört und die nie wieder vergeben wird, kannst du sie ansprechen, ändern und löschen, ohne mitzuzählen.
Zum Mitnehmen
Diese Lektion hat keine Aufgabe, sondern drei Beispiele und eine Frage: An welcher Stelle hört eine JSON-Datei auf zu tragen? Die Antwort steht in den ersten beiden Beispielen nebeneinander, und sie ist kürzer, als du denkst.
Jetzt du
Basis Konto, kostenlosIm Editor änderst du die Beispiele dieser Lektion und lässt sie gleich laufen. So merkst du am schnellsten, ob es sitzt.
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 3 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.