Best Practice: Alphabetische Liste in PHP (z. B. für ein Glossar)

Geschrieben vor elf Jahre und zehn Monate.
Die Lesezeit beträgt etwa drei Minuten und zwölf Sekunden.

Für ein Projekt benötige ich eine alphabetische Liste, wie man sie gerne in einem Glossar benutzt. Im Code des von mir favorisierten Plug-ins bin ich auf ein „Problem“ gestoßen, welches ich vor einiger Zeit erst meinem Auszubildenden erklärt habe.

Die Aufgabe

Wir benötigen ein Array, welches den ersten Buchstaben aller in einer Datenbank stehenden Namen in alphabetischer Reihenfolge enthält. Mit diesen Daten wird eine alphabetische Liste ähnlich der folgenden von Bundestag.de realisiert:

Glossar von Bundestag.de

Der Code

In $oSql haben wir ein mysqli-Objekt mit einer erfolgreichen Verbindung zur Datenbank zur Verfügung. PDO funktioniert natürlich auch.

Der folgende Code ist nicht das Original. Zur besseren Ansicht habe ich diesen vereinfacht. Das Vorgehen entspricht jedoch exakt dem des Plug-ins:

if ($oRes = $oSql->query('SELECT name FROM users')) {
    $aAlphabet = array();
    while ($aRow = $oRes->fetch_object()) {
        array_push($aAlphabet, substr($aRow->name, 0, 1));
    }
}
$aAlphabet = array_unique($aAlphabet);
sort($aAlphabet);
print_r($aAlphabet);

Der Autor der Erweiterung holt alle Namen aus der Datenbank. Anschließend wird ein leeres Array $aAlphabet definiert. Bei jedem Durchlauf des Ergebnisses der Datenbankabfrage fügt er mit array_push dem zuvor definierten Array den ersten Buchstaben des Ergebnisses hinzu. Dieser Buchstabe wird via substr() abgeschnitten. Danach wird das Array via array_unique() von doppelten Einträgen befreit und via sort() sortiert.

Mit meinen Testdaten (nur 100 Namen in der Datenbank) dauern 100.000 Durchläufe 84.3804 Sekunden.

Nun läuft dieser Code normalerweise nur ein Mal und nicht 100.000 Mal durch. Was bedeutet, der Autor ist hier zwar am Ziel und hat die Aufgabe gelöst, aber man kann die Sache wie immer verbessern. Die folgende Variante lagert die meiste Arbeit in die Datenbankabfrage aus und spart außerdem einige Zeilen Code:

if ($oRes = $oSql->query('SELECT DISTINCT(LEFT(UPPER(name), 1)) AS alphabet FROM users ORDER BY name ASC')) {
    $aAlphabet = array();
    while ($aRow = $oRes->fetch_object()) {
        $aAlphabet[] = $aRow->alphabet;
    }
}
print_r($aAlphabet);

Statt die vollständige Namen holen wir mit LEFT(name, 1) nur den ersten Buchstaben aus der Datenbank. Dies ersetzt die PHP-Funktion substr(). UPPER() sorgt dafür, dass das Ergebnis in Großbuchstaben zurückgegeben wird. DISTINCT() entfernt uns die doppelten Einträge und erspart ein array_unique(). ORDER BY name ASC sortiert direkt nach dem Alphabet und macht sort() überflüssig. Außerdem verzichten wir auf die Funktion array_push() und nutzen $aAlphabet[] =, um das Element dem Array anzuhängen.

Diese Variante dauerte 59.7660 Sekunden und ist damit knapp 41 % schneller als die vorherige Version.

Und wenn wir schon dabei sind …

Immer mal wieder sehe ich folgende Zeile im Code (ebenfalls im erwähnten Plug-in):

$aAlphabet = array('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z');

Hier kann man sich einiges an Schreibarbeit sparen, wenn folgende Funktion genutzt wird:

$aAlphabet = range('A', 'Z');

Hier sei erwähnt, dass ein Funktionsaufruf von range() langsamer ist als das direkte Definieren des Arrays. Dies ist aber minimal, außerdem wird solch ein Array normalerweise nur ein Mal definiert.


Kommentar schreiben


Manuel Wenner vor elf Jahre und zehn Monate:
Cooler Beitrag! Kurz und knackig geschrieben, top! Ach ja und das mit dem LEFT bei DB Abfragen weis ich ab jetzt auch ;)

Thorsten Walk vor elf Jahre und zehn Monate:
Interessanter Artikel! Alternativ würde auch 'substr(name,1,1)' gehen. Bei meinen lokalen DB's war das sogar nen ticken schneller wie left(). Kannst ja mal testen @Fabian

Fabian Beiner vor elf Jahre und zehn Monate:
Thorsten, ganz korrekt - SUBSTRING() würde auch funktionieren. Ob es schneller ist, habe und werde ich nicht testen. LEFT() ist explizit für diese Aufgabe da: Gib x Zeichen eines Strings von links zurück. SUBSTRING() ist mächtiger, dürfte aber mit mehr "Overhead" verbunden sein (intern), da hier die Funktion erst entscheiden muss, was sie tun soll.

Michael vor elf Jahre und acht Monate:
Hi Fabian, sehr schöner Artikel. Vor allem bei einer sehr großen Datenbank dürfte die neue Variante deutlich schneller sein. Ein kleiner Fehler ist mir aufgefallen: "DISTINCT() entfernt uns die doppelten Einträge und erspart ein array_sort()" Du meinst wahrscheinlich array_unique().

Fabian Beiner vor elf Jahre und acht Monate:
Danke Michael. Sowohl fürs Lob als für den Hinweis - du hast natürlich Recht, hab den Fehler gefixt. Und ja, bei großen Mengen macht es einen Unterschied, bei kleinen Datenmengen ist es eher nicht relevant. Trotzdem: Besser gleich "richtig" machen. ;)

Andrey vor neun Jahre und sieben Monate:
"Im Code des von mir favorisierten Plug-ins "... Was für ein Plug-in ? Ich bräuchte eigentlich die ganze Code für eine alphabetische Listdarstellung....

Fabian Beiner vor neun Jahre und sieben Monate:
Andrey, es handelte sich um irgendein beliebiges WordPress-Plugin. Ich weiß nicht mehr genau, welches. Was du genau vorhast, wird mir aus deinem Beitrag leider nicht ersichtlich, sorry.