PHP MySQL Order By

Sorting, multi-column order, and why the sort column needs an allowlist.

Beginner
Key PointsSort columns cannot be bound — validate against an allowlist and interpolate.

Basic ordering

PHP
<?php
$stmt = $pdo->query("SELECT name, age FROM users ORDER BY age DESC");
foreach ($stmt->fetchAll() as $r) {
    echo "{$r['name']} {$r['age']}\n";
}

echo "---\n";

$stmt = $pdo->query("SELECT name, age FROM users ORDER BY name ASC");
foreach ($stmt->fetchAll() as $r) {
    echo $r["name"] . "\n";
}
Output
Bob 28
Tom 30
Amy 25
---
Amy
Bob
Tom

Multi-column ordering

PHP
<?php
$stmt = $pdo->query(
    "SELECT name, status, age FROM users
     ORDER BY status ASC, age DESC"
);
foreach ($stmt->fetchAll() as $r) {
    echo "{$r['status']} {$r['name']} {$r['age']}\n";
}
Output
1 Bob 28
1 Tom 30
2 Amy 25

Sort by the first column, then by the second when tied. Order is priority — reversing it changes the result.

The sort column needs an allowlist

ORDER BY ? is a syntax error — identifiers cannot be bound. Interpolating is an injection point. An allowlist map is the only safe route.

PHP
<?php
$allowed = [
    "id"    => "id",
    "name"  => "name",
    "age"   => "age",
    "time"  => "created_at",
];

$sort = $_GET["sort"] ?? "id";
$dir  = strtolower($_GET["dir"] ?? "asc") === "desc" ? "DESC" : "ASC";

if (!isset($allowed[$sort])) {
    $sort = "id";
}

$column = $allowed[$sort];
$sql = "SELECT name FROM users ORDER BY $column $dir";

echo $sql, "\n";
Output
SELECT name FROM users ORDER BY created_at DESC
The allowlist means user input is only a lookup key, never part of the SQL. Even sort=id;DROP TABLE users fails isset and falls back.
PHP
<?php
// 错误写法演示(不要用)
$bad = "id; DROP TABLE users";
$sql = "SELECT name FROM users ORDER BY $bad";
echo $sql, "\n";

// 白名单后
$allowed = ["id" => "id", "name" => "name"];
$col = $allowed[$bad] ?? "id";
echo "SELECT name FROM users ORDER BY $col", "\n";
Output
SELECT name FROM users ORDER BY id; DROP TABLE users
SELECT name FROM users ORDER BY id

Where NULLs sort

MySQL treats NULL as smallest: first in ASC, last in DESC. Reorder explicitly if needed.

PHP
<?php
$sql1 = "SELECT name FROM users ORDER BY age ASC";
$sql2 = "SELECT name FROM users ORDER BY age IS NULL, age ASC";

echo $sql1, "\n";
echo $sql2, "\n";
Output
SELECT name FROM users ORDER BY age ASC
SELECT name FROM users ORDER BY age IS NULL, age ASC

age IS NULL yields 0 or 1, so sorting by it puts non-nulls first — MySQL's equivalent of NULLS LAST.

Sorting with paging

PHP
<?php
$page = max(1, (int)($_GET["page"] ?? 1));
$size = 2;
$offset = ($page - 1) * $size;

$total = (int)$pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
$pages = (int)ceil($total / $size);

$sql = sprintf("SELECT name FROM users ORDER BY id LIMIT %d OFFSET %d", $size, $offset);
$rows = $pdo->query($sql)->fetchAll(PDO::FETCH_COLUMN);

echo "第 $page/$pages 页: " . implode(", ", $rows) . "\n";
Output
第 1/2 页: Tom, Amy
Unstable sorts (many equal keys) make rows appear on two pages or none. Always add a unique tiebreaker: ORDER BY status, id.

Performance

  • Index the sort column —— otherwise MySQL uses filesort
  • Match composite index order —— (status, created_at) supports that ORDER BY
  • **ORDER BY RAND() sorts the whole table** —— use a random ID offset instead
  • **Check EXPLAIN for Using filesort** —— its presence means no index sort
PHP
<?php
// ORDER BY RAND() 的替代:先取随机起点
$total = (int)$pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
$offset = random_int(0, max(0, $total - 3));

$sql = sprintf("SELECT name FROM users ORDER BY id LIMIT %d, 3", $offset);
echo $sql, "\n";
Output
SELECT name FROM users ORDER BY id LIMIT 0, 3

Friends & Partners