Blog / Coding tips

PHP Sort Multidimensional Array by Value (usort, Tested)

Cover image for PHP Sort Multidimensional Array by Value (usort, Tested)

To sort a multidimensional array by value in PHP, use usort() with the spaceship operator: usort($rows, fn($a, $b) => $a['grade'] <=> $b['grade']);. That sorts a list of rows by the grade key from lowest to highest. Swap $a and $b on the right-hand side for descending order. To sort by several columns, compare arrays: [$b['grade'], $a['name']] <=> [$a['grade'], $b['name']] sorts by grade (high to low) and then by name (A to Z). usort() sorts the array in place and renumbers the keys, so use uasort() if your outer keys are IDs you need to keep. For a quick one-column sort, array_multisort(array_column($rows, 'grade'), SORT_DESC, $rows) does the same job.

I ran every example in this guide on PHP 8.4.26 on 7 October 2026, with full error reporting switched on, and pasted the real output under each one. The sample data is a small list of students with names, grades, UK-format join dates and cities. It's deliberately awkward: two students share a grade, one name starts with a lower-case letter and one starts with "Ö". Those are the cases where simple sorting code goes wrong.

The sample data

Every example loads this file. The names() helper just prints the names in their current order, so the output stays short.

<?php
$students = [
    ['name' => 'Priya',  'grade' => 72, 'joined' => '14/09/2025', 'city' => 'Leeds'],
    ['name' => 'Tom',    'grade' => 65, 'joined' => '02/10/2024', 'city' => 'Cardiff'],
    ['name' => 'Amira',  'grade' => 72, 'joined' => '03/01/2026', 'city' => 'Belfast'],
    ['name' => 'Ölsen',  'grade' => 58, 'joined' => '21/02/2025', 'city' => 'Leeds'],
    ['name' => 'callum', 'grade' => 81, 'joined' => '30/06/2025', 'city' => 'Glasgow'],
];
function names(array $rows): string {
    return implode(', ', array_map(fn($r) => $r['name'], $rows));
}

A "multidimensional array" in PHP usually means exactly this: a list (the outer array) of associative arrays (the rows). It's what you get from PDOStatement::fetchAll(PDO::FETCH_ASSOC), from json_decode($json, true) and from reading a CSV with headers. PHP's built-in sort() doesn't know which key inside each row to look at, which is why you need a comparison function.

Sort by one key with usort() and the spaceship operator

usort() takes the array (by reference) and a callback. PHP calls the callback with two rows, $a and $b, and the callback returns a negative number if $a should come first, a positive number if $b should come first, and zero if they're equal. The spaceship operator <=> returns exactly those three values, so one arrow function is all you need.

<?php
require __DIR__ . '/data.php';

$byGrade = $students;
usort($byGrade, fn($a, $b) => $a['grade'] <=> $b['grade']);
echo "grade asc:  ", names($byGrade), "\n";

usort($byGrade, fn($a, $b) => $b['grade'] <=> $a['grade']);
echo "grade desc: ", names($byGrade), "\n";

echo "keys after usort: ", implode(',', array_keys($byGrade)), "\n";
grade asc:  Ölsen, Tom, Priya, Amira, callum
grade desc: callum, Priya, Amira, Tom, Ölsen
keys after usort: 0,1,2,3,4
  • Ascending: $a['grade'] <=> $b['grade'] puts Ölsen (58) first and callum (81) last.
  • Descending: $b['grade'] <=> $a['grade'], with $a and $b swapped, reverses it. You don't need array_reverse() afterwards, and you shouldn't use it, because it also reverses the order of ties.
  • Keys are renumbered: after usort(), the keys are 0, 1, 2, 3, 4 in the new order. That's fine for a list, but it loses any meaningful keys, as I show later.

usort() returns true and changes the array you pass in. It doesn't return the sorted array, which catches a lot of people out (see the mistakes section). If you need to keep the original order too, copy the array first: in PHP, $copy = $rows; makes a real copy of an array, so sorting $copy leaves $rows alone.

Ties keep their original order

Priya and Amira both have 72. Here's a descending sort by grade only:

<?php
require __DIR__ . '/data.php';
// Priya and Amira both have 72. PHP 8+ sorts are stable, so Priya (first in input) stays first.
usort($students, fn($a, $b) => $b['grade'] <=> $a['grade']);
echo names($students), "\n";
callum, Priya, Amira, Tom, Ölsen

Priya comes before Amira because she's before Amira in the original data. Since PHP 8.0, every PHP sort is stable: rows that compare as equal keep their existing order. Before PHP 8.0 the order of ties was undefined, so old code that "happened to work" might not have been guaranteed to. Stability is useful, because you can sort by a secondary column first and the primary column second, but it's clearer to say what you mean with a multi-column comparison.

Sort by multiple columns

To sort by grade and then by name when grades are equal, compare two-element arrays. PHP compares arrays element by element, so the second value only matters when the first ones are equal.

<?php
require __DIR__ . '/data.php';

// grade high-to-low, then name A-Z
usort($students, fn($a, $b) =>
    [$b['grade'], $a['name']] <=> [$a['grade'], $b['name']]);
foreach ($students as $s) {
    echo mb_str_pad($s["name"], 8), $s["grade"], "\n";
}
callum  81
Amira   72
Priya   72
Tom     65
Ölsen   58

callum is top with 81. Amira and Priya are tied on 72, and now Amira comes first because of the name tie-break. The trick for mixing directions is where you put $a and $b: in the first position $b is on the left (grade descending), and in the second position $a is on the left (name ascending). You can extend the arrays to three or four columns the same way.

I use mb_str_pad() here (new in PHP 8.3) rather than str_pad() or printf("%-8s"), because "Ölsen" is six bytes but five characters, and byte-based padding would knock the column out of line. It's a small thing, but it's the first sign that text with accents needs extra care when sorting too.

A reusable sortBy() helper

If you sort in several places, a small helper keeps the callbacks out of your templates. This one takes the column names and directions as an array, compares strings case-insensitively and numbers numerically, and treats missing keys as null instead of raising a warning.

<?php
require __DIR__ . '/data.php';

/**
 * Sort a list of rows by one or more columns.
 * $spec: ['grade' => 'desc', 'name' => 'asc']
 */
function sortBy(array $rows, array $spec): array
{
    usort($rows, function (array $a, array $b) use ($spec): int {
        foreach ($spec as $col => $dir) {
            $cmp = is_string($a[$col] ?? null)
                ? strcasecmp($a[$col], $b[$col] ?? '')
                : ($a[$col] ?? null) <=> ($b[$col] ?? null);
            if ($cmp !== 0) {
                return strtolower($dir) === 'desc' ? -$cmp : $cmp;
            }
        }
        return 0;
    });
    return $rows;
}

echo names(sortBy($students, ['city' => 'asc', 'grade' => 'desc'])), "\n";
echo names(sortBy($students, ['grade' => 'desc', 'name' => 'asc'])), "\n";
echo "original untouched: ", names($students), "\n";
Amira, Tom, callum, Priya, Ölsen
callum, Amira, Priya, Tom, Ölsen
original untouched: Priya, Tom, Amira, Ölsen, callum

The first call sorts by city A to Z, then grade high to low within each city, so the two Leeds students appear with Priya (72) before Ölsen (58). The second call reproduces the grade-then-name sort. Because the function receives its own copy of the array and returns the sorted copy, the original order is untouched, as the last line shows. That makes the helper safe to call on data you'll use again, such as rows from a database query. If those rows come from user-chosen sort options, never put the column name straight into SQL; check it against a fixed list, as I explain in my guide to PDO prepared statements.

Sort alphabetically: case, accents and natural order

Sorting by a string column looks easy, but the default comparisons work on bytes, not on the alphabet as people expect it.

<?php
require __DIR__ . '/data.php';

$a = $students;
usort($a, fn($x, $y) => strcmp($x['name'], $y['name']));
echo "strcmp:       ", names($a), "\n";

usort($a, fn($x, $y) => strcasecmp($x['name'], $y['name']));
echo "strcasecmp:   ", names($a), "\n";

$files = ['img12.png', 'img10.png', 'img2.png'];
sort($files);
echo "sort:         ", implode(', ', $files), "\n";
natsort($files);
echo "natsort:      ", implode(', ', $files), "\n";

if (class_exists('Collator')) {
    $c = new Collator('en_GB');
    usort($a, fn($x, $y) => $c->compare($x['name'], $y['name']));
    echo "Collator en_GB: ", names($a), "\n";
}
strcmp:       Amira, Priya, Tom, callum, Ölsen
strcasecmp:   Amira, callum, Priya, Tom, Ölsen
sort:         img10.png, img12.png, img2.png
natsort:      img2.png, img10.png, img12.png
Collator en_GB: Amira, callum, Ölsen, Priya, Tom
  1. strcmp() compares bytes, and every capital letter sorts before every lower-case letter. So "Tom" comes before "callum". The spaceship operator on strings does the same.
  2. strcasecmp() ignores case for plain English letters, so callum moves to second place. But "Ölsen" is still last, because "Ö" is a multi-byte character whose first byte is higher than any English letter.
  3. natsort() (and strnatcmp() / strnatcasecmp() inside usort()) sort numbers inside strings by value, so img2.png comes before img10.png. A plain sort() put img10 and img12 first. Use natural order for file names, version numbers and anything like "Room 9" and "Room 10".
  4. Collator('en_GB') from PHP's intl extension sorts the way a UK reader expects: case doesn't matter and "Ölsen" sits with the O names, between callum and Priya. This is the right choice for names, towns and anything shown to people. You need the intl extension (on Debian and Ubuntu, the php8.4-intl package), which I had to install for this test.

If you're sorting the same data in the browser as well, JavaScript has the same problem and the same fix, Intl.Collator('en-GB'), which I cover in my post on how to sort an array of objects by property in JavaScript.

Sort by date (UK dd/mm/yyyy format)

Dates stored as UK strings are a classic trap, because sorting them as text compares the day first.

<?php
require __DIR__ . '/data.php';

$wrong = $students;
usort($wrong, fn($a, $b) => strcmp($a['joined'], $b['joined']));
echo "as strings (wrong): ", implode(', ', array_column($wrong, 'joined')), "\n";

$toTime = fn(string $d) => DateTimeImmutable::createFromFormat('!d/m/Y', $d, new DateTimeZone('Europe/London'));
usort($students, fn($a, $b) => $toTime($b['joined']) <=> $toTime($a['joined']));
echo "newest first:       ", implode(', ', array_column($students, 'joined')), "\n";
as strings (wrong): 02/10/2024, 03/01/2026, 14/09/2025, 21/02/2025, 30/06/2025
newest first:       03/01/2026, 14/09/2025, 30/06/2025, 21/02/2025, 02/10/2024

As strings, 02/10/2024 came first and 30/06/2025 last, which is just the day numbers in order: 02, 03, 14, 21, 30. That's nonsense as a date order. The fix is to turn each string into a DateTimeImmutable with createFromFormat('!d/m/Y', ...) and compare those objects, which the spaceship operator does properly. The ! resets the time to midnight, so two dates on the same day compare as equal instead of depending on the current time. With $b on the left, the newest join date comes first.

On large arrays, convert each date once before sorting rather than inside the callback, because usort() calls the callback many times per row. Better still, store dates as Y-m-d (ISO 8601) or as DATE columns in the database: ISO strings sort correctly as plain text, and the database can sort them for you with ORDER BY. For the time zone side of UK dates, including British Summer Time, see my guide to setting the PHP time zone to Europe/London.

Keep keys with uasort(), and when to use array_multisort()

Associative arrays: asort, arsort, ksort and uasort

When the keys mean something (a product name, a user ID), usort() throws them away. PHP has a sort function for each situation:

<?php
$prices = ['tea' => 3.20, 'coffee' => 2.80, 'scone' => 2.95];

$v = $prices; asort($v);   echo "asort:  ", json_encode($v), "\n";
$v = $prices; arsort($v);  echo "arsort: ", json_encode($v), "\n";
$v = $prices; ksort($v);   echo "ksort:  ", json_encode($v), "\n";
$v = $prices; usort($v, fn($a, $b) => $a <=> $b); echo "usort:  ", json_encode($v), "\n";

$byId = [
    'u17' => ['name' => 'Priya', 'grade' => 72],
    'u03' => ['name' => 'Tom',   'grade' => 65],
    'u42' => ['name' => 'Callum','grade' => 81],
];
uasort($byId, fn($a, $b) => $b['grade'] <=> $a['grade']);
echo "uasort: ", implode(', ', array_keys($byId)), "\n";
asort:  {"coffee":2.8,"scone":2.95,"tea":3.2}
arsort: {"tea":3.2,"scone":2.95,"coffee":2.8}
ksort:  {"coffee":2.8,"scone":2.95,"tea":3.2}
usort:  [2.8,2.95,3.2]
uasort: u42, u17, u03
  • asort() sorts by value, low to high, and keeps the keys.
  • arsort() does the same, high to low.
  • ksort() sorts by key, so here alphabetically by product name. krsort() reverses it.
  • usort() sorted the values but replaced the keys with 0, 1, 2. The product names are gone, so the JSON became a plain list.
  • uasort() is usort() that keeps keys. The students keyed by ID were sorted by grade, and the IDs u42, u17 and u03 survived. This is the answer to "php sort array by value preserve keys" for rows.

array_multisort() with array_column()

array_multisort() sorts one or more column arrays and reorders the main array to match. With array_column() it reads almost like SQL:

<?php
require __DIR__ . '/data.php';

$grades = array_column($students, 'grade');
$names  = array_column($students, 'name');
array_multisort($grades, SORT_DESC, $names, SORT_ASC, SORT_STRING | SORT_FLAG_CASE, $students);
echo names($students), "\n";
callum, Amira, Priya, Tom, Ölsen

This sorted by grade descending, then name ascending ignoring case (SORT_STRING | SORT_FLAG_CASE), and gave the same result as my usort() version. It's fast because it doesn't call a PHP function for every comparison. The downsides are that it's harder to read, it modifies the column arrays as well as the main array, and it can't do custom logic such as date parsing or collation. I use usort() by default and switch to array_multisort() only for big arrays where profiling shows the sort matters. If the data came from a database in the first place, though, the best sort is usually ORDER BY in the query, which can use an index.

Quick reference: which sort function?

  • List of rows, sort by one or more keys: usort() with <=>.
  • Rows keyed by ID, keep the IDs: uasort().
  • Sort by the outer keys themselves: ksort() or krsort(), or uksort() with your own comparison.
  • Flat array of values: sort() or rsort() for lists, asort() or arsort() to keep keys.
  • Very large list, simple columns, speed matters: array_multisort() with array_column().
  • Names and words shown to people: Collator from intl, called inside usort().
  • File names or labels with numbers in them: natsort(), or strnatcasecmp() inside usort().

All of these sort the array you pass in and return true, so none of them can be chained or used in an expression.

Sorting an array of objects

Modern PHP code often uses small classes instead of associative arrays, for example data transfer objects built from a database row or an API response. The same functions work; you just read properties with -> instead of keys.

<?php
final class Student
{
    public function __construct(
        public readonly string $name,
        public readonly int $grade,
    ) {}
}

$list = [new Student('Priya', 72), new Student('Tom', 65), new Student('callum', 81)];

usort($list, fn(Student $a, Student $b) => $b->grade <=> $a->grade);
echo implode(', ', array_map(fn($s) => "$s->name ($s->grade)", $list)), "\n";

// Reusable comparator built once, passed by name
function byNameCi(Student $a, Student $b): int { return strcasecmp($a->name, $b->name); }
usort($list, byNameCi(...));
echo implode(', ', array_column($list, 'name')), "\n";
callum (81), Priya (72), Tom (65)
callum, Priya, Tom

The first sort orders the students by grade, high to low, with typed parameters (Student $a, Student $b) so PHP throws a clear TypeError if something else slips into the list. The second sort uses a named function passed with PHP 8.1's first-class callable syntax, byNameCi(...). That's handy when the same comparison is used in several places, because the function can be unit-tested on its own and the name documents what the sort does.

Two details are worth knowing. First, array_column() works on arrays of objects too, reading public properties, which is how the second line prints the names without a loop. Second, usort() moves the objects around but doesn't copy them, so the sorted array holds the same objects as the original. With readonly properties, as here, that can't cause surprises. With mutable objects, changing one through the sorted array also changes it everywhere else it's referenced, because objects in PHP are passed by handle, unlike arrays, which are copied on assignment.

Common mistakes when sorting arrays in PHP

<?php
require __DIR__ . '/data.php';

// 1. Using the return value
$result = usort($students, fn($a, $b) => $a['grade'] <=> $b['grade']);
var_dump($result);

// 2. Returning a bool instead of an int
$rows = [['n' => 3], ['n' => 1], ['n' => 2]];
try {
    usort($rows, fn($a, $b) => $a['n'] > $b['n']);
} catch (Throwable $e) {
    echo get_class($e), "\n";
}
echo implode(',', array_column($rows, 'n')), "\n";

// 3. Missing key
$rows = [['n' => 2], ['x' => 9], ['n' => 1]];
usort($rows, fn($a, $b) => ($a['n'] ?? PHP_INT_MAX) <=> ($b['n'] ?? PHP_INT_MAX));
echo json_encode($rows), "\n";

// 4. Numeric strings
$rows = [['v' => '10'], ['v' => '9'], ['v' => '100']];
usort($rows, fn($a, $b) => strcmp($a['v'], $b['v']));
echo "strcmp: ", implode(',', array_column($rows, 'v')), "\n";
usort($rows, fn($a, $b) => (int) $a['v'] <=> (int) $b['v']);
echo "int:    ", implode(',', array_column($rows, 'v')), "\n";
bool(true)

Deprecated: usort(): Returning bool from comparison function is deprecated, return an integer less than, equal to, or greater than zero in mistakes.php on line 11
1,2,3
[{"n":1},{"n":2},{"x":9}]
strcmp: 10,100,9
int:    9,10,100
  1. Using the return value. $result = usort(...) gives you bool(true), not the sorted array. The array you passed in has been sorted.
  2. Returning a boolean from the callback. $a['n'] > $b['n'] returns true or false. PHP 8 still sorts the array, but raises the deprecation notice shown above, and the behaviour isn't guaranteed. Always return an integer, ideally with <=>.
  3. Missing keys. If one row has no n, reading $a['n'] raises an "Undefined array key" warning. Use $a['n'] ?? PHP_INT_MAX to push incomplete rows to the end, as in the example.
  4. Numeric strings compared as text. Values from forms, CSV files and some database drivers are strings. strcmp() sorted '10', '100', '9'. Cast them with (int) or (float), or compare with <=> on numbers.
  5. Sorting UK dates as strings. Convert them to DateTimeImmutable or store them as ISO dates.
  6. Using usort() on keyed data. It throws keys away. Use uasort().
  7. Reversing after a sort for descending order. Swap $a and $b in the comparison instead, so ties stay in a sensible order.
  8. Printing sorted user data without escaping. Sorting doesn't make names safe for HTML. Wrap each value with htmlspecialchars(), as in my guide to preventing XSS with htmlspecialchars.

FAQ

How do I sort a multidimensional array by a specific key in PHP?

Use usort() with a callback that compares that key: usort($rows, fn($a, $b) => $a['price'] <=> $b['price']);. The spaceship operator returns -1, 0 or 1, which is what usort() needs. The array is sorted in place and its keys are renumbered from 0. The PHP manual's usort() page has more examples, including one that sorts by last name and then first name.

How do I sort a PHP array by value in descending order?

For a list of rows, swap the operands: usort($rows, fn($a, $b) => $b['grade'] <=> $a['grade']);. For a simple array of values, use rsort(), or arsort() if you need to keep the keys. With array_multisort(), pass SORT_DESC after the column.

How do I sort by two fields with usort?

Compare arrays of values: usort($rows, fn($a, $b) => [$a['last'], $a['first']] <=> [$b['last'], $b['first']]);. PHP compares the first elements, and only looks at the second when the first are equal. To mix directions, swap $a and $b for the column you want descending.

What is the difference between usort and uasort?

Both sort with your own comparison function. usort() renumbers the keys 0, 1, 2 and so on, so it's for lists. uasort() keeps each value's original key, so it's for associative arrays where keys are IDs or names. In my test, uasort() kept the student IDs u42, u17 and u03, while usort() on a keyed price list lost the product names.

How do I sort a multidimensional array by date in PHP?

Convert the dates to DateTimeImmutable objects and compare them with <=>. For UK dates, use DateTimeImmutable::createFromFormat('!d/m/Y', $date). Don't compare dd/mm/yyyy strings directly, because that sorts by the day number. ISO dates (2026-10-07) do sort correctly as text.

If you're returning sorted data from an API, my PHP REST API tutorial shows how to send it back as JSON with the right status codes. If clients can choose the sort column with a query parameter, check it against a fixed allow-list before using it, exactly as you would for SQL.

// note

How to read this note.

This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.

If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.

Related notes