Navigation
 Startseite
 Fachbücher
 Anzeigenmarkt
 Forum
 Webmaster News
 Script Newsletter
 Push Meldungen
 Kontakt
 Script Installation
 Php
 Php Tutorials
 Lernpfade
 Webhoster Vergleich
 Impressum

Community-Bereich
 kostenlos Registrieren
 Anmelden
 Benutzerliste

Script Datenbank
 Script Archiv
 Script Top 20
 Screenshots
 Testberichte

Suche
 

Unsere Php Scripts
 Counter Script
 Umfrage Script
 Bilder Upload Script
 Terminverwaltung
 Simple PHP Forum
 RSS Grabber

Tools und Generatoren
 .htpasswd Generator
 md5 Generator
 base64 Generator
 Markdown to HTML
 Colorpicker
 Unix timestamp Tool
 Unit Test Generator
 TLD Liste
 Webkatalog‑Verzeichnis

Artfiles.de
Bietet Serviceorientierte...
https://www.Artfiles.de
Hosterplus.de
Bekommen Sie Speicherplatz (Webspace), Domains...
https://www.Hosterplus.de
 
 
 

PHP mysqli_affected_rows() Function: UPDATE richtig prüfen

Sie befinden sich: Home > Php Tutorial > PHP mysqli_affected_rows() ...

PHP mysqli_affected_rows() Function: UPDATE richtig prüfen
Veröffentlicht am:
15.10.2026
Hits / Besucher:
7
Sprache:
  Deutsch
Tutorial Art:
eigenes
Eingetragen von:
Merkliste:
 
Beschreibung

Ein Verwaltungsskript löscht ein Buch über seine ID. Die ID stammt aus einem alten Lesezeichen, das Buch ist längst entfernt, und trotzdem meldet die Seite „gelöscht“. Der Grund: mysqli_query() gibt bei einem DELETE true zurück, sobald die Anweisung fehlerfrei durchlief. Ob sie dabei auch nur eine Zeile getroffen hat, steht in diesem Wert nicht. Das verrät erst PHP mysqli_affected_rows, allerdings mit einigen Eigenheiten, die man kennen muss, bevor man aus der Zahl Schlüsse zieht. Verbindungsaufbau und mysqli-Grundlagen setzt dieser Text voraus.

Illustration zum Tutorial: PHP mysqli_affected_rows() Function: UPDATE richtig prüfen

Im Normalfall ist die Zahl genau das, wonach sie aussieht: drei geänderte Zeilen ergeben eine 3. Interessant wird es bei der 0 und bei der -1.

Was PHP mysqli_affected_rows() zählt

Die Funktion liefert die Zahl der Zeilen, die die letzte schreibende Abfrage auf dieser Verbindung verändert hat, also das letzte INSERT, UPDATE, REPLACE oder DELETE. Der Bezug ist die Verbindung, nicht eine bestimmte Abfrage. Auf einer frischen Verbindung steht vor der ersten Abfrage eine 0. Am Beispiel mit der verschwundenen Buch-ID wird der Unterschied zwischen „lief fehlerfrei“ und „hat etwas bewirkt“ sichtbar:

<?php

$sql = 'DELETE FROM buch WHERE id = 999';
$ok = mysqli_query($db, $sql);
var_dump($ok); /* bool(true) */

$zahl = mysqli_affected_rows($db);
printf("%d rows affected\n", $zahl); /* 0 */

if ($zahl === 0) {
echo 'Dieses Buch gibt es nicht.';
} else {
echo 'Buch gelöscht.';
}

Bei einem DELETE ist die 0 eindeutig, es gab schlicht keine passende Zeile. Bei einem UPDATE ist sie es nicht, und an diesem Unterschied hängt der wichtigste Abschnitt weiter unten.

Die Signatur im Original

<?php

/* Procedural style */
mysqli_affected_rows(mysqli $mysql): int|string

/* Object-oriented style */
$mysqli->affected_rows; /* int|string */

/* Prepared statements */
mysqli_stmt_affected_rows(
mysqli_stmt $statement
): int|string
$stmt->affected_rows; /* int|string */

/*
* $mysql The mysqli connection object
* (not the result of mysqli_query()).
* $statement The executed mysqli_stmt object.
*
* Returns the number of rows affected by the last
* INSERT, UPDATE, REPLACE or DELETE query.
* 0 = no rows changed, -1 = query failed
* (only reachable with MYSQLI_REPORT_OFF).
*
* The number of affected rows always belongs to
* the query that ran last on this connection.
* For a buffered SELECT the function returns the
* number of rows in the result set.
*/

Der Rückgabetyp int|string wirkt ungewöhnlich. Einen string liefert PHP mysqli_affected_rows laut Handbuch nur, wenn die Zahl größer als PHP_INT_MAX wäre, im Alltag kommt ein Integer zurück. Wichtiger ist der Parameter: Die Funktion erwartet die Verbindung. Wer von der alten Funktion mysql_affected_rows() umsteigt, übergibt gern das Ergebnis von mysqli_query(). Das ist bei einem UPDATE nur true, und PHP bricht mit TypeError: mysqli_affected_rows(): Argument #1 ($mysql) must be of type mysqli, true given ab.

Bei PHP mysqli_affected_rows liefern prozedurale und objektorientierte Schreibweise denselben Wert. Das folgende UPDATE zählt einen Aufrufzähler hoch, damit sich die Werte bei jedem Lauf tatsächlich ändern:

<?php

$sql = "UPDATE beitrag SET aufrufe = aufrufe + 1
WHERE status = 'offen'";

/* prozedural */
mysqli_query($db, $sql);
echo mysqli_affected_rows($db);

/* objektorientiert */
$db->query($sql);
echo $db->affected_rows;

Warum ein UPDATE 0 Zeilen meldet

Ein Nutzer öffnet sein Profil, ändert nichts und klickt trotzdem auf „Speichern“. Das UPDATE findet die Zeile, schreibt dieselben Werte hinein, und PHP mysqli_affected_rows meldet 0. MySQL zählt standardmäßig nur Zeilen, deren Inhalt sich wirklich ändert. Gefunden heißt nicht geändert. Wer die 0 als „Datensatz existiert nicht“ liest, zeigt in genau diesem Fall eine falsche Fehlermeldung an. In der Zeile mit der ID 7 steht vorher der Ort Bremen:

<?php

$sql = "UPDATE nutzer SET ort = 'Kiel'
WHERE id = 7";

$db->query($sql);
echo $db->affected_rows; /* 1 */

$db->query($sql);
echo $db->affected_rows; /* 0 */
echo mysqli_info($db);
/* Rows matched: 1 Changed: 0 Warnings: 0 */

mysqli_info() zeigt beide Zahlen nebeneinander: Rows matched sind die gefundenen Zeilen, Changed die geänderten. Wie man PHP mysqli_affected_rows nach einer schreibenden Abfrage insgesamt liest, fasst das folgende Diagramm zusammen.

flowchart TD
    A[UPDATE oder DELETE] --> B{Fehler?}
    B -- ja --> C[Exception ab PHP 8.1]
    C -.-> D[-1 nur bei REPORT_OFF]
    B -- nein --> E[affected_rows lesen]
    E --> F{"Wert > 0?"}
    F -- ja --> G[Zeilen geändert]
    F -- nein --> H{Werte schon gleich?}
    H -- ja --> I[gefunden, nicht geändert]
    H -- nein --> J[kein Treffer]
    I -.-> K[FOUND_ROWS oder mysqli_info]

Aus der 0 führen zwei Wege heraus. Der erste wertet mysqli_info() aus und holt sich die Zahl der gefundenen Zeilen aus dem Text. Das bleibt auf die eine Stelle beschränkt und ändert sonst nichts:

<?php

$info = mysqli_info($db) ?? '';
preg_match('/matched: (\d+)/', $info, $m);
$gefunden = (int) ($m[1] ?? 0); /* 1 */

Der zweite Weg setzt beim Verbinden das Flag MYSQLI_CLIENT_FOUND_ROWS. Das geht nur über mysqli_init() und mysqli_real_connect(), nicht über new mysqli(). Danach meldet dasselbe zweite UPDATE eine 1.

<?php

$db = mysqli_init();
$db->real_connect(
'localhost', 'app', $pass, 'shop',
3306, null, MYSQLI_CLIENT_FOUND_ROWS
);

$db->query($sql); /* Werte schon gleich */
echo $db->affected_rows; /* 1 */

Dieser Schalter ist kein harmloser Komfort. Er gilt für die ganze Verbindung, also auch für Code, der sich auf die Standardzählung verlässt. Bei ON DUPLICATE KEY UPDATE verschiebt er ebenfalls das Ergebnis, dazu mehr in der Tabelle unten. In bestehendem Code ist mysqli_info() an der einen Stelle deshalb meist die sicherere Wahl.

-1 und Fehler seit PHP 8.1

In vielen älteren Beispielen steht eine Prüfung auf -1. Seit PHP 8.1 ist der Meldemodus MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT voreingestellt, eine fehlerhafte Abfrage wirft also eine mysqli_sql_exception. Ein Vergleich des Ergebnisses von PHP mysqli_affected_rows mit -1 wird dann nie erreicht. Nur wer den Meldemodus abschaltet, bekommt die alte Antwort zu sehen:

<?php

mysqli_report(MYSQLI_REPORT_OFF);

$ok = $db->query('UPDATE gibtsnicht SET a = 1');
var_dump($ok); /* bool(false) */
var_dump($db->affected_rows); /* int(-1) */

Für die Auswertung heißt das: 0 ist kein Fehler, sondern ein Ergebnis. Fehler kommen heute als Ausnahme, und wie man sie an ihrer Nummer unterscheidet, steht im Tutorial zu mysqli_errno() und Fehlernummern. Wer Altcode mit == -1 pflegt und den Ausnahmemodus aktiv hat, kann diese Zweige entfernen.

affected_rows bei Prepared Statements in PHP mysqli

Bei vorbereiteten Anweisungen tritt $stmt->affected_rows an die Stelle von PHP mysqli_affected_rows, gelesen direkt nach dem execute(). Im Test stimmte $stmt->affected_rows mit dem Wert an der Verbindung überein, beide Quellen funktionieren also. Das Statement ist trotzdem die bessere Wahl, weil der Wert dort eindeutig zu genau dieser Anweisung gehört. Wie Platzhalter und bind_param() funktionieren, erklärt das Tutorial zu mysqli Prepared Statements.

<?php

$stmt = $db->prepare(
'UPDATE nutzer SET newsletter = ? WHERE id = ?'
);
$stmt->bind_param('ii', $abo, $id);
$stmt->execute();

printf("%d rows affected\n", $stmt->affected_rows);
/* prozedural gleichwertig: */
echo mysqli_stmt_affected_rows($stmt);

Auch hier gilt die Regel aus dem vorigen Abschnitt. Schickt das Formular denselben Wert, der schon in der Spalte steht, meldet das Statement 0, obwohl die ID existiert.

INSERT, REPLACE und ON DUPLICATE KEY UPDATE

Bei einfachen Anweisungen entspricht die Zahl der Erwartung. Bei den Varianten, die einfügen oder ersetzen, weicht PHP mysqli_affected_rows davon ab, und wer daraus eine Importstatistik baut, zählt schnell doppelt. Alle Werte der Tabelle sind mit PHP 8.4 gegen MySQL 8.0.46 gemessen.

Anweisung Situation Ergebnis Bemerkung
INSERT mit drei Wertgruppen alle Zeilen neu 3 eine je Zeile
INSERT IGNORE mit drei Zeilen eine davon ist eine Dublette 2 übersprungene Zeile zählt nicht
REPLACE INTO Schlüssel noch frei 1 wie ein INSERT
REPLACE INTO Schlüssel schon vorhanden, andere Werte 2 alte Zeile gelöscht, neue eingefügt; bei identischen Werten gemessen 1
REPLACE INTO Primärschlüssel und eindeutiger Schlüssel treffen zwei Zeilen 3 zwei gelöscht, eine eingefügt
INSERT … ON DUPLICATE KEY UPDATE Zeile neu 1 eingefügt
INSERT … ON DUPLICATE KEY UPDATE vorhandene Zeile geändert 2 aktualisiert
INSERT … ON DUPLICATE KEY UPDATE vorhandene Zeile hat schon dieselben Werte 0 mit FOUND_ROWS 1
TRUNCATE TABLE Tabelle war gefüllt 0 Tabelle wird neu angelegt, nicht zeilenweise geleert

Die 2 bei REPLACE ist kein Zählfehler. REPLACE aktualisiert nicht, es löscht die alte Zeile und fügt eine neue ein. Schickt man dieselben Werte noch einmal, meldete MySQL 8.0 bei einer Tabelle, die nur einen Primärschlüssel hat, allerdings 1. Das Löschen hat Folgen über die Zahl hinaus: Im Versuch räumte ein Fremdschlüssel mit ON DELETE CASCADE dabei die abhängigen Zeilen ab, und kollidiert ein eindeutiger Schlüssel neben einer Auto-Increment-Spalte, bekommt die Zeile eine neue ID. Zeilen, die ein Trigger zusätzlich in eine andere Tabelle schreibt, zählen dagegen nicht mit. Im Code sieht der Vergleich so aus:

<?php

$sql = "REPLACE INTO preis (sku, betrag)
VALUES ('A-1', %s)";

$db->query(sprintf($sql, '9.90'));
echo $db->affected_rows; /* 1 */
$db->query(sprintf($sql, '9.95'));
echo $db->affected_rows; /* 2 */

$sql = "INSERT INTO lager (sku, menge)
VALUES (?, ?)
ON DUPLICATE KEY UPDATE
menge = VALUES(menge)";
/* neu: 1, geändert: 2, gleich: 0 */

Den Wert sofort sichern

Ein Wartungsskript löscht abgelaufene Sitzungen, schreibt danach eine Zeile ins Protokoll und liest erst dann die Zahl ab. Im Monitoring steht seitdem jeden Tag eine 1. Die Zahl gehört zum Protokoll-INSERT, nicht zum DELETE. Jede weitere Abfrage auf derselben Verbindung überschreibt den Wert, gemessen sogar ein SET NAMES, das ihn auf 0 setzt. PHP mysqli_affected_rows gehört deshalb in die Zeile direkt nach der Abfrage, und das Ergebnis in eine Variable.

Große Löschvorgänge laufen außerdem besser in Blöcken. Ein DELETE mit LIMIT hält Sperren kurz, und die Zeilenzahl sagt, wann Schluss ist: Kommt 0 zurück, ist nichts mehr zu löschen. Mit 2500 abgelaufenen Sitzungen läuft die Schleife viermal und meldet am Ende 2500.

<?php

$stmt = $db->prepare(
'DELETE FROM sitzung WHERE ablauf < ? LIMIT 1000'
);
$grenze = date('Y-m-d H:i:s');
$stmt->bind_param('s', $grenze);

$gesamt = 0;
do {
$stmt->execute();
$runde = $stmt->affected_rows; /* sofort sichern */
$gesamt += $runde;
} while ($runde > 0);

error_log("$gesamt Sitzungen entfernt");

Praxis: Lagerbestand per UPDATE abbuchen

Ein Shop verkauft das letzte Exemplar eines Artikels, und zwei Bestellungen kommen fast gleichzeitig an. Wer erst per SELECT den Bestand abfragt und dann bucht, lässt eine Lücke: Die zweite Bestellung liest dieselbe 1, und der Bestand läuft ins Minus. Sicherer ist ein bedingtes UPDATE, das Prüfung und Änderung in einer Anweisung erledigt. Das Ergebnis von PHP mysqli_affected_rows trägt dann die fachliche Entscheidung.

<?php

$stmt = $db->prepare(
'UPDATE artikel SET bestand = bestand - 1
WHERE id = ? AND bestand > 0'
);
$stmt->bind_param('i', $artikelId);
$stmt->execute();

if ($stmt->affected_rows === 1) {
echo 'Gebucht.';
} else {
echo 'Ausverkauft oder unbekannt.';
}

Bei einem Artikel mit Bestand 1 liefert der erste Aufruf eine 1, der zweite eine 0. Die Falle mit unveränderten Werten kann hier nicht zuschnappen, denn jeder Treffer ändert den Bestand. Eine 0 bedeutet also wirklich: nichts mehr da oder Artikel unbekannt.

Praxis: optimistisches Sperren mit Versionsspalte

Zwei Redakteure öffnen dasselbe Projekt, beide ändern den Titel, und wer zuletzt speichert, überschreibt den anderen ohne Warnung. Eine Versionsspalte verhindert das ohne Tabellensperre. Das Formular schickt die Version mit, die beim Öffnen galt, und das UPDATE greift nur, wenn sie noch stimmt:

<?php

$stmt = $db->prepare(
'UPDATE projekt
SET titel = ?, version = version + 1
WHERE id = ? AND version = ?'
);
$stmt->bind_param('sii', $titel, $id, $version);
$stmt->execute();

if ($stmt->affected_rows === 0) {
echo 'Inzwischen von jemand anderem geändert.';
}

Weil sich die Version bei jedem erfolgreichen Speichern erhöht, gibt es auch hier keine Zeile, die gefunden, aber nicht geändert wird. Meldet PHP mysqli_affected_rows hier eine 0, war jemand schneller, oder das Projekt wurde gelöscht. Im Konfliktfall klärt ein gezieltes Nachlesen der ID, welcher der beiden Fälle vorliegt.

Abgrenzung: SELECT und mysqli_num_rows()

Für lesende Abfragen ist mysqli_num_rows() zuständig, mysqli_affected_rows() gehört zu allem, was Daten verändert. In Foren steht oft, PHP mysqli_affected_rows funktioniere auch bei einem SELECT. Bei einer gepufferten Abfrage stimmt das zufällig, gemessen kommt die Zeilenzahl der Ergebnismenge heraus. Bei einer ungepufferten Abfrage mit MYSQLI_USE_RESULT steht vor dem Lesen aber -1 und danach 0. Wer Ergebnismengen zählen oder erneut durchlaufen will, findet das im Tutorial zu mysqli_data_seek() und mysqli_result. Innerhalb von MySQL, etwa in gespeicherten Prozeduren, liefert die SQL-Funktion ROW_COUNT() dieselbe Auskunft.

Fazit

Ein true von mysqli_query() sagt nur, dass die Anweisung fehlerfrei lief. Was sie bewirkt hat, verrät PHP mysqli_affected_rows, und zwar als Zahl geänderter, nicht gefundener Zeilen. Die 0 nach einem UPDATE ist deshalb mehrdeutig, bis man mysqli_info() oder MYSQLI_CLIENT_FOUND_ROWS hinzunimmt. Die -1 begegnet einem heute nur noch bei abgeschaltetem Meldemodus. REPLACE zählt doppelt, weil es löscht und neu einfügt. Und jede weitere Abfrage überschreibt den Wert. Am meisten leistet die Zahl dort, wo sie eine Entscheidung trägt: bei der Abbuchung ohne vorheriges SELECT und beim optimistischen Sperren mit Versionsspalte.

 


 

Kommentare (0)

Noch keine Kommentare. Sei der Erste!

Melde dich an, um einen Kommentar zu schreiben.
Bücherregal mit drei Büchern: 'PHP 4 - Grundlagen und Profiwissen' von Hanser Verlag, 'Webdesign in a Nutshell' von O'Reilly Verlag, und 'Webgestaltung' von Galileo Computing.