Abschnitt 14 · Lektion 3
Abfragen
In Lektion 14.2 hast du Daten hineingeschrieben. Jetzt holst du sie wieder heraus, und dafür gibt es in PDO eine Methode und drei Arten, das Ergebnis entgegenzunehmen.
$db->query($sql) schickt die Abfrage los und gibt dir ein Ergebnisobjekt zurück, ein
PDOStatement. Darin stecken die gefundenen Zeilen, aber du kommst nicht direkt an sie heran: Du
musst sie abholen. Genau dieser zweite Schritt ist der, den man am Anfang vergisst.
Merk dir für diese Lektion eine Einschränkung: query() ist nur für Abfragen gedacht, in denen
kein Wert von außen steckt. Sobald ein Suchwort aus einem Formular oder eine Nummer aus der
Adresszeile in die Abfrage soll, ist query() der falsche Weg, und der richtige steht in Lektion
14.4. Alle Abfragen hier stehen deshalb fest im Quelltext.
fetchAll oder fetch
<?php
// Legt die Tabelle an und fuellt sie mit acht Notizen. Dieselbe
// Verbindung wie in Lektion 14.2, nur mit einer Spalte mehr.
//
// Die Tabelle wird bei jedem Lauf neu aufgebaut, damit du immer
// denselben Stand vor dir hast. 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,
angelegt TEXT NOT NULL
)");
$einfuegen = $db->prepare("INSERT INTO notizen (titel, erledigt, angelegt)
VALUES (:titel, :erledigt, :angelegt)");
foreach ([
["Tee", 0, "2026-08-17"],
["Seife", 0, "2026-08-18"],
["Kerzen", 1, "2026-08-19"],
["Batterien", 0, "2026-08-20"],
["Kaffee", 0, "2026-08-22"],
["Zahnpasta", 1, "2026-08-25"],
["Brot", 0, "2026-08-23"],
["Milch", 0, "2026-08-24"],
] as [$titel, $erledigt, $angelegt]) {
$einfuegen->execute([":titel" => $titel, ":erledigt" => $erledigt, ":angelegt" => $angelegt]);
} <?php
// Dieselbe Abfrage, zweimal abgeholt.
require __DIR__ . "/vorbereiten.php";
$alle = $db->query("SELECT id, titel FROM notizen ORDER BY id LIMIT 3")->fetchAll();
echo "fetchAll() liefert eine Liste von Zeilen:\n";
print_r($alle);
$eine = $db->query("SELECT id, titel FROM notizen ORDER BY id LIMIT 3")->fetch();
echo "\nfetch() liefert nur die erste, und zwar direkt als Zeile:\n";
print_r($eine);
echo "\nDeshalb kommst du an den Titel unterschiedlich heran:\n";
echo " aus fetchAll: ", $alle[0]["titel"], "\n";
echo " aus fetch: ", $eine["titel"], "\n"; fetchAll() holt alle Zeilen auf einmal und gibt dir ein Array von Arrays. Genau die Form, die
du in Lektion 5.3 als verschachteltes Array kennengelernt hast: außen eine Liste mit Nummern, innen
je eine Zuordnung von Spaltenname auf Wert. Ein foreach darüber gibt dir eine Zeile nach der
anderen.
fetch() holt eine Zeile, nämlich die nächste, und gibt sie dir direkt als Zuordnung. Du
kommst also mit $zeile["titel"] an den Wert und nicht mit $zeilen[0]["titel"].
Die Faustregel dazu ist kurz: Erwartest du eine Liste, nimm fetchAll(). Erwartest du genau eine
Zeile, etwa weil du nach einer id suchst, nimm fetch().
Es gibt noch einen dritten Weg, und der ist bequem, wenn dich nur ein einziger Wert interessiert.
fetchColumn() gibt dir die erste Spalte der ersten Zeile, also aus
SELECT count(*) FROM notizen direkt die Zahl statt eines Arrays mit einem Eintrag darin.
Vier Wörter, mit denen du fast alles fragen kannst
<?php
// Legt die Tabelle an und fuellt sie mit acht Notizen. Dieselbe
// Verbindung wie in Lektion 14.2, nur mit einer Spalte mehr.
//
// Die Tabelle wird bei jedem Lauf neu aufgebaut, damit du immer
// denselben Stand vor dir hast. 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,
angelegt TEXT NOT NULL
)");
$einfuegen = $db->prepare("INSERT INTO notizen (titel, erledigt, angelegt)
VALUES (:titel, :erledigt, :angelegt)");
foreach ([
["Tee", 0, "2026-08-17"],
["Seife", 0, "2026-08-18"],
["Kerzen", 1, "2026-08-19"],
["Batterien", 0, "2026-08-20"],
["Kaffee", 0, "2026-08-22"],
["Zahnpasta", 1, "2026-08-25"],
["Brot", 0, "2026-08-23"],
["Milch", 0, "2026-08-24"],
] as [$titel, $erledigt, $angelegt]) {
$einfuegen->execute([":titel" => $titel, ":erledigt" => $erledigt, ":angelegt" => $angelegt]);
} <?php
// Dieselbe Abfrage in vier Stufen. Sieh zu, wie das Ergebnis
// kleiner wird und sich dreht.
require __DIR__ . "/vorbereiten.php";
$stufen = [
"ohne Zusatz" => "SELECT titel FROM notizen",
"WHERE erledigt = 0" => "SELECT titel FROM notizen WHERE erledigt = 0",
"dazu ORDER BY angelegt DESC" => "SELECT titel FROM notizen WHERE erledigt = 0 ORDER BY angelegt DESC",
"dazu LIMIT 3" => "SELECT titel FROM notizen WHERE erledigt = 0 ORDER BY angelegt DESC LIMIT 3",
];
foreach ($stufen as $was => $sql) {
$titel = [];
foreach ($db->query($sql) as $zeile) {
$titel[] = $zeile["titel"];
}
echo $was, "\n";
echo " ", count($titel), " Zeilen: ", implode(", ", $titel), "\n\n";
} Das Beispiel baut dieselbe Abfrage in vier Stufen auf, und du siehst bei jeder, was sie ändert.
SELECT titel FROM notizen ist die Grundform: Welche Spalten aus welcher Tabelle. Ein * statt der
Spaltennamen heißt „alle Spalten” und ist zum Ausprobieren praktisch. In fertigem Code schreibst du
lieber auf, was du wirklich brauchst, dann fällt beim Lesen sofort auf, womit die Zeile arbeitet.
WHERE erledigt = 0 ist die Bedingung. Aus acht Zeilen werden sechs. Du kannst Bedingungen mit
AND und OR verbinden, und es gibt LIKE für Textsuche mit Platzhalter, wo % für beliebig
viele Zeichen steht.
ORDER BY angelegt DESC sortiert. Die Zahl der Zeilen ändert sich nicht, nur die Reihenfolge dreht
sich um: DESC heißt absteigend, ASC aufsteigend, und ohne Angabe gilt aufsteigend. Sortiert wird
nach dem, was in der Spalte steht, und weil unsere Daten als 2026-08-24 abgelegt sind, sortiert
sich das Datum von selbst richtig.
LIMIT 3 schneidet ab. Erst jetzt werden aus sechs Zeilen drei, und die Reihenfolge davor
entscheidet, welche drei. Ein LIMIT ohne ORDER BY gibt dir irgendwelche drei Zeilen, und heute
sind es vielleicht andere als morgen.
Im Reiter Debug siehst du die vier Stufen an titel: Die Liste wächst beim ersten Durchlauf auf
acht Einträge, beim zweiten auf sechs, beim dritten wieder auf sechs, nur fängt sie jetzt mit
Milch an, und beim vierten bleibt es bei drei.
Was zurückkommt, wenn nichts da ist
<?php
// Legt die Tabelle an und fuellt sie mit acht Notizen. Dieselbe
// Verbindung wie in Lektion 14.2, nur mit einer Spalte mehr.
//
// Die Tabelle wird bei jedem Lauf neu aufgebaut, damit du immer
// denselben Stand vor dir hast. 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,
angelegt TEXT NOT NULL
)");
$einfuegen = $db->prepare("INSERT INTO notizen (titel, erledigt, angelegt)
VALUES (:titel, :erledigt, :angelegt)");
foreach ([
["Tee", 0, "2026-08-17"],
["Seife", 0, "2026-08-18"],
["Kerzen", 1, "2026-08-19"],
["Batterien", 0, "2026-08-20"],
["Kaffee", 0, "2026-08-22"],
["Zahnpasta", 1, "2026-08-25"],
["Brot", 0, "2026-08-23"],
["Milch", 0, "2026-08-24"],
] as [$titel, $erledigt, $angelegt]) {
$einfuegen->execute([":titel" => $titel, ":erledigt" => $erledigt, ":angelegt" => $angelegt]);
} <?php
// Eine Abfrage, die nichts findet. Drei Wege, drei Rueckgaben.
require __DIR__ . "/vorbereiten.php";
$sql = "SELECT * FROM notizen WHERE titel = 'Marmelade'";
echo "fetch(): "; var_dump($db->query($sql)->fetch());
echo "fetchAll(): "; var_dump($db->query($sql)->fetchAll());
echo "fetchColumn(): "; var_dump($db->query($sql)->fetchColumn());
echo "\nfalse ist nicht null:\n";
echo " false === null: ", var_export(false === null, true), "\n";
echo " false == null: ", var_export(false == null, true), "\n";
echo "\nUnd so sieht es aus, wenn du den Fall vergisst:\n";
$nichts = $db->query($sql)->fetch();
echo " Titel: ";
var_dump($nichts["titel"]);
echo " Sieh dir dazu den Fehlerkanal im Terminal an.\n"; Hier lauert die Falle dieser Lektion. fetchAll() gibt dir bei null Treffern ein leeres Array,
und damit läuft ein foreach einfach nullmal durch. Kein Sonderfall, nichts zu tun.
fetch() gibt dir dagegen false. Nicht null, nicht ein leeres Array, sondern false. Und
weil du in Lektion 3.6 gelernt hast, wie großzügig == in PHP vergleicht, ist die Zeile aus dem
Beispiel wichtig: false == null ist wahr, false === null ist falsch. Prüfst du mit == null,
fängst du also mehr, als du wolltest.
Deshalb lautet die Regel: Prüf das Ergebnis von fetch() mit === false. Das ist eindeutig, es
verwechselt nichts, und es hält auch dann, wenn in einer Spalte tatsächlich einmal eine 0 oder eine
leere Zeichenkette steht.
Vergisst du die Prüfung, greift dein Code mit $zeile["titel"] auf false zu. PHP macht daraus ein
null und schreibt eine Warnung auf den Fehlerkanal: Trying to access array offset on false.
Die letzte Zeile des Beispiels zeigt genau das, und im Terminal steht die Warnung mitten in der
Ausgabe. Mit php index.php 2>/dev/null siehst du die Ausgabe ohne den Fehlerkanal, dann bleibt
dort nur Titel: NULL.
Direkt über das Ergebnis laufen
Im zweiten Beispiel ist dir vielleicht aufgefallen, dass dort kein fetchAll() steht. Das
PDOStatement lässt sich nämlich auch direkt in ein foreach stecken, und dann kommt eine Zeile
nach der anderen.
Der Unterschied ist der Speicher. fetchAll() holt alles auf einmal, und bei zwanzigtausend Zeilen
liegen die alle gleichzeitig da. Das foreach über das Statement holt sich eine Zeile, wenn es sie
braucht. Bei drei Zeilen ist das egal, und fetchAll() liest sich besser. Bei sehr vielen Zeilen ist
es der Unterschied zwischen läuft und läuft nicht.
Zum Mitnehmen
Eine Abfrage besteht aus zwei Hälften, und die zweite vergisst man leicht: Erst fragst du, dann holst du das Ergebnis ab. Wie du abholst, entscheidet, was du in der Hand hast.
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 3 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.