PHP: Wie sortiert man ein Array nach einer Spalte?

Datensätze nach einer Spalte sortieren ist eine Zeile. Die ältere Variante mit dem Minuszeichen, die man in vielen Antworten noch findet, funktioniert allerdings bei genau dem Datentyp nicht, den man am häufigsten sortiert: Preisen.

Die Antwort

<?php
  usort($server, fn($a, $b) => $a['preis'] <=> $b['preis']);
?>
aufsteigend:  VPS 200/4.99  VPS 500/4.99  RS 2000/12.99  RS 4000/29.99

Absteigend drehst du die beiden Seiten um:

<?php
  usort($server, fn($a, $b) => $b['preis'] <=> $a['preis']);
?>

Der <=> heißt Raumschiff-Operator und liefert -1, 0 oder 1. Genau das, was usort() erwartet.

Warum nicht mit Minus

In älteren Antworten steht statt <=> oft das hier:

<?php
  usort($server, fn($a, $b) => $a['preis'] - $b['preis']);   // kaputt
?>

Als Begründung, warum das schlecht sei, liest man meist “Integer-Überlauf”. Der kommt in der Praxis aber selten vor. Der Fall, den man wirklich trifft, ist ein anderer:

$x['preis'] - $y['preis'] : -0.5
als int gecastet          : 0   <- 0 heißt "gleich"!
mit <=>                   : -1

PHP castet den Rückgabewert des Vergleichs-Callbacks auf int. -0.5 wird dabei zu 0, und 0 bedeutet für usort(): Die beiden Elemente sind gleichwertig.

Das heißt: Jede Preisdifferenz unter einem Euro verschwindet. 4,99 und 5,49 gelten als gleich. Bei einer Preisliste ist damit ungefähr die Hälfte aller Vergleiche kaputt, und das Ergebnis sieht auf den ersten Blick trotzdem plausibel aus — es ist ja grob sortiert.

Bei ganzen Zahlen funktioniert die Minus-Variante. Aber es gibt keinen Grund, sie zu verwenden, seit es <=> gibt.

Nach zwei Spalten sortieren

Wenn zwei Datensätze denselben Preis haben, willst du oft ein zweites Kriterium. Die naheliegende Lösung ist eine if-Kaskade. Es geht kürzer:

<?php
  usort($server, fn($a, $b) =>
      [$a['preis'], $a['name']] <=> [$b['preis'], $b['name']]);
?>
Preis, dann Name: VPS 200/4.99  VPS 500/4.99  RS 2000/12.99  RS 4000/29.99

<=> vergleicht Arrays elementweise und bricht beim ersten Unterschied ab. Das entspricht exakt “erst nach Preis, bei Gleichstand nach Name” und liest sich besser als drei verschachtelte if.

Sortierung ist seit PHP 8.0 stabil

Ein Detail, das man kennen sollte, weil es alten Code betrifft:

gleiche Preise: erster/5  zweiter/5  dritter/5

Elemente mit gleichem Sortierwert behalten ihre ursprüngliche Reihenfolge. Das ist seit PHP 8.0 garantiert. Davor war es das nicht — die Reihenfolge hing von der Implementierung ab und konnte sich zwischen PHP-Versionen ändern.

Praktisch heißt das: Wenn du nach Preis sortierst und die Liste vorher schon nach Name sortiert war, ist sie danach “nach Preis, bei Gleichstand nach Name” sortiert. Ohne dass du das zweite Kriterium hinschreiben musst.

Verlassen würde ich mich darauf trotzdem nur, wenn beide Sortierungen direkt beieinander stehen. Sonst schreibt jemand später eine Zeile dazwischen und wundert sich.

array_multisort() und array_column()

Die zweite verbreitete Variante:

<?php
  array_multisort(array_column($server, 'preis'), SORT_ASC, $server);
?>

Funktioniert und ist kurz. Zwei Dinge stören mich daran: Das Array wird per Referenz umgebaut, und bei mehreren Kriterien wird der Aufruf schnell unleserlich. Ich bleibe bei usort().

array_column() für sich ist dagegen sehr nützlich:

array_column($server, 'preis')         : [12.99,4.99,29.99,4.99]
array_column($server, 'preis', 'name') : {"RS 2000":12.99,"VPS 200":4.99,...}
array_column($server, null, 'name')    : Schlüssel sind jetzt die Namen

Die letzte Variante — null als zweiter Parameter — schlüsselt ganze Datensätze nach einer Spalte um. Das braucht man ständig, wenn man zwei Ergebnismengen einander zuordnen will, und es ist deutlich schneller als eine Schleife mit foreach.

Strings sortieren: zwei Überraschungen

Erstens die Sache mit den Zahlen im Text:

sort()       : ["Item2","item10","item100","item9"]
SORT_NATURAL : ["Item2","item9","item10","item100"]

sort() vergleicht Zeichen für Zeichen, und 1 kommt vor 9. Deshalb landet item100 vor item9. Mit SORT_NATURAL (oder natsort()) wird die Zahl als Zahl gelesen. Bei Dateinamen, Versionsnummern und allem, was durchnummeriert ist, willst du das.

Zweitens — und das ist der Punkt, den ich hier eigentlich machen will — die Umlaute:

sort()          : ["Apfel","Baum","Zebra","Ähre"]
Collator de_DE  : ["Ähre","Apfel","Baum","Zebra"]

Ähre landet hinter Zebra. Der Grund ist, dass sort() Bytes vergleicht. In UTF-8 ist Ä die Bytefolge 0xC3 0x84, und 0xC3 ist größer als das 0x5A von Z. Alle Umlaute, alle Akzente, alles außerhalb von ASCII landet also hinten.

Für eine Liste, die ein Mensch liest — Namen, Orte, Produkte — brauchst du deshalb Collator aus der Erweiterung intl:

<?php
  $col = new Collator('de_DE');
  $col->sort($namen);
?>

Das sortiert nach den Regeln der jeweiligen Sprache, und die sind nicht überall gleich: Im Deutschen wird Ä wie A einsortiert, im Schwedischen steht es ganz am Ende des Alphabets.

Ob intl verfügbar ist, prüfst du mit class_exists('Collator'). Bei den meisten Hostern ist die Erweiterung dabei, garantiert ist es nicht.

usort() wirft die Schlüssel weg

Zum Schluss noch ein Detail, das bei assoziativen Arrays beißt:

usort  keys : [0,1,2]         <- die Schlüssel sind weg
uasort keys : ["b","c","a"]   <- erhalten

usort() vergibt die Schlüssel neu. Wenn deine Datensätze nach ID oder Name verschlüsselt sind, brauchst du uasort(). Und wenn du nach den Schlüsseln selbst sortieren willst, uksort().

Die Merkregel: a steht für “associative” (Schlüssel bleiben), k für “key” (nach Schlüsseln sortieren).

Zusammenfassung

  • usort($daten, fn($a, $b) => $a['spalte'] <=> $b['spalte']).
  • Niemals mit -: Der Rückgabewert wird auf int gecastet, und jede Differenz unter 1 wird damit zu “gleich”.
  • Zwei Kriterien: [$a['x'], $a['y']] <=> [$b['x'], $b['y']].
  • Seit PHP 8.0 ist die Sortierung stabil.
  • array_column($daten, null, 'spalte') zum Umschlüsseln.
  • SORT_NATURAL für Zahlen im Text.
  • Collator für Umlaute — sort() stellt Ä hinter Z.
  • uasort() statt usort(), wenn die Schlüssel erhalten bleiben sollen.

Hinweis zu Netcup (Werbung)

Der deutsche Hoster Netcup bietet unter anderem günstige und zugleich leistungsstarke Webhosting Pakete, KVM-basierte Root Server und dezidierte Server an. Mit unseren Gutscheincodes kannst du noch mehr Geld sparen (6€ bei deiner ersten Bestellung, 30% Rabatt auf alle KVM-basierten Root Server, ...).