MySQL unter PHP debuggen mit einer eigenen MySQL Klasse
Bei einem meiner Projekte kam es zu ungewöhnlich langen Ladezeiten eines Formulars. Da hier allerdings auch sehr viele Datenbankabfragen ausgeführt werden müssen und dabei auch noch die Werte per COUNT() gezählt werden wunderten mich die Zeiten nicht wirklich.
Nun wollte ich natürlich wissen, welche Abfrage dabei besonders viel Zeit in Anspruch nimmt um dann gezielt optimieren zu können. Ich setze in dem Projekt auf die MySQLi Klasse von PHP. Da ich hierbei die Klasse objektorientiert nutze, war es sehr einfach möglich diese zu erweiterten. Eine Möglichkeit wäre es natürlich gewesen eine eigene Funktion zu schreiben, aber in diesem Fall hätte ich auch alle Skript anpassen müssen, in denen ich eine Query ausführe. Daher habe ich mich dazu entschlossen einfach die query() Funktion von MySQLi zu überschreiben. Hier ein stark vereinfachtes Beispiel, wie so etwas aussehen könnte:
<?php
class mysqliDebugger extends mysqli {
public $queries = array();
public $queries_time = 0;
function __construct($host, $user, $pass, $name, $port, $sock){
parent::__construct($host, $user, $pass, $name, $port, $sock);
}
function __destruct(){
parent::__destruct();
}
function query($query){
$start = microtime(true);
if(!$result = parent::query($query)){
$error_message = $this->error;
}
$end = microtime(true);
$this->queries[] = array(
'number' => count($this->queries),
'query' => $query,
'error' => $error_message,
'time' => $end - $start
);
$this->queries_time += $end - $start;
return $result;
}
}
$mysqli = new mysqliDebugger('host', 'user', 'pass', 'name');
if($result = $mysqli->query('SELECT * FROM tablename')){
while($row = $result->fetch_assoc()){
// Verarbeiten der Ergebniszeilen
}
}
if($result = $mysqli->query('SELECT * FROM tablename2')){
while($row = $result->fetch_assoc()){
// Verarbeiten der Ergebniszeilen
}
}
?>
Sehen wir uns die wichtigsten Zeilen des Quellcodes an. Die Zeilen 8-10 zeigen den Konstruktor unserer neuen Klasse, die den Konstruktor der geerbten Klasse MySQLi aufruft. In den Zeilen 16-36 überschreiben wir die query() Funktion der MySQLi Klasse mit unserer Debugging-Klasse. Dort wird zuerst einmal zur Protokollierung der Ausführungszeit der Startzeitpunkt mit Hilfe der microtime() Funktion ermittelt. Anschließend wird der Query mit dem Aufruf parent::query($query) an die query() Funktion der MySQLi Klasse übergeben. Einen eventuell auftretenden Fehler speichern wir dabei in einer Variablen ab.
In Zeile 23 speichern wir die Endzeit des Query. Anschließend speichern wir alle Werte, die wir zur späteren Auswertung benötigen in einem Array gespeichert. Zusätzlich summieren wir noch alle Zeiten auf, um später die Gesamtdauer aller Statements bestimmen zu können. Die ab Zeile 38 wird eine einfache Instanziierung und Nutzung der Klasse aufgezeigt, wie man sie auch von MySQLi kennt. Wer also in seinen Skripten schon MySQLi verwendet kann einfach beim Instanziieren der Klasse die neue Debugging-Klasse verwenden und muss keine weiteren Zeilen im Quellcode ändern. Ich empfehle eine kleine Bedingung zu verwenden, die feststellt, ob der Debugging-Modus aktiv ist und nur dann die Debugging-Klasse zu verwenden, wenn sie auch benötigt wird. Das spart im produktiven Einsatz etwas Rechenzeit. Eine mögliche Bedingung könnte wie folgt aussehen:
if($debug){
$mysqli = new mysqliDebugger('host', 'user', 'pass', 'name');
} else {
$mysqli = new mysqli('host', 'user', 'pass', 'name');
}
Anstelle der $debug Variablen könntet ihr auch einen GET Parameter oder einen Session Parameter verwenden. Ich rate aber dringend davon ab einen einfach zu erratenden GET Parameter zu verwenden, da in solchen Fällen eure SQL-Statements für Unbefugte sichtbar werden könnten, was unter Umständen die Sicherheit eueres Systems gefährden könnte.
Nachdem wir die Daten gesammelt haben möchten wir sie natürlich auch ausgeben. Das solltet ihr auch wieder die vorherige Bedingung verwenden. Dazu erstellen wir einfach eine Tabelle, die die gesammelten Daten enthält:
<table style="border: 1px solid #000;" rules="all"> <caption>Gesamtdauer: <?= $mysqli->queries_time ?></caption> <tr> <th>Anzahl</th> <th>Query</th> <th>Fehler</th> <th>Zeit</th> </tr> <? foreach($mysqli->queries as $query) : ?> <tr> <td><?= $query['number'] ?></td> <td><?= $query['query'] ?></td> <td><?= $query['error'] ?></td> <td><?= $query['time'] ?></td> </tr> <? endforeach ?> </table>
Nehmen wir an, wir die Tabelle aus dem ersten Statement ist in der Datenbank nicht vorhanden. In diesem Fall würden wir einen SQL-Fehler erhalten. In der Tabelle können wir dann direkt den Fehler ermitteln und entsprechend das Statement korrigieren. Auch hier wird das Statement gespeichert und die Zeit gemessen.
| Anzahl | Query | Fehler | Zeit |
|---|---|---|---|
| 0 | SELECT * FROM tablename | Table ‘name.tablename’ doesn’t exist | 0.00076198577880859 |
| 1 | SELECT * FROM tablename2 | 0.0018200874328613 |
Ihr habt mit dieser kleinen Beispiel-Klasse einen sehr einfachen Debugger zur Hand, mit dem ihr sehr schnell die langsamen Statements in eurem System finden könnt. Ihr könnt die Klasse natürlich auch noch nach euren Wünschen erweitern und auch andere Funktionen von MySQLi überschreiben. Wenn ihr, so wie ich, zentral an einer Stelle die Datenbankverbindung erzeugt könnt ihr durch die Bedingung aus dem zweiten Quellcode eure gesamte Applikation debuggen.
Ich habe damit in meinem Projekt schon zwei überflüssige Statements finden können und einige andere Statements noch weiter optimieren können. Ich hoffe, dass euch der Debugger auch bei eurer täglichen Arbeit unterstützen kann. Über Anregungen und Kommentare Wrede ich mich wie immer freuen.
Arrays und andere komplexe Daten mit PHP in einer MySQL-Datenbank speichern
Viele von euch werden wohl schon einmal vor dem Problem gestanden haben, dass sie ein Array oder ein Objekt in der Datenbank speichern mussten. Hier möchte ich ein paar Vorschläge unterbreiten, wie man das Problem nicht lösen sollte und wie es besser gehen kann.
Der schlechte Weg
Die einfachste und gleichzeitig auch schlechteste Methode wäre es, für jeden Index eines Arrays oder jede Eigenschaft eines Objekts eine neue Spalte zu erzeugen. Bei diesem Ansatz werden unter Umständen viele Zeilen erzeugt, die nicht immer einen Wert enthalten. Das ist zwar nicht so gravierend, aber durch diesen Ansatz erhöht sich auch die Anzahl der Spalten schnell auf eine unübersichtliche Anzahl. Zuletzt ist es hierbei bei jeder Änderung des Arrays oder Objekts notwendig die Datenbanktabelle anzupassen.
Suchen-und-Ersetzen mit MySQL-Datenbanken
Vor ein paar Tagen musste ich ca. 1000 Datensätze, die fehlerhaft in eine Datenbank geschrieben wurden überarbeiten. In einer Spalte, die Links enthält musste die Toplevel-Domain von .de auf .com geändert werden. Bis zu diesem Zeitpunkt war ich der Meinung, dass es nicht möglich ist ein Suchen-und-Ersetzen auf MySQL-Tabellen mit einem einfachen SQL-Statement durchzuführen. In der Regel habe ich daher die Tabelle mit einem Programm wie Access verbunden und dort die Suchen-und-Ersetzen Funktion genutzt.
Die Lösung für das Problem war allerdings recht simpel. Ich habe für Abfragen schon mehrfach die REPLACE() Funktion von MySQL benutzt, die wie folgt definiert ist:








