跳到主要内容

行排序(ORDER BY)

关系表本身没有可依赖的行顺序。只有查询显式使用 ORDER BY,数据库才保证按指定规则返回结果:

SELECT order_id, customer_id, total_amount, created_at
FROM orders
ORDER BY created_at DESC, order_id DESC;

即使某次查询碰巧按主键、插入时间或索引顺序返回,下次执行也可能因执行计划、并行查询、数据变化或数据库升级而不同。

基本语法​

ORDER BY 写在查询结果形成之后,通常位于 LIMIT、OFFSET 或 FETCH 之前:

SELECT column_list
FROM table_expression
ORDER BY sort_expression [ASC | DESC];

ASC 表示升序,DESC 表示降序。省略方向时通常默认为 ASC:

-- 金额从小到大
ORDER BY total_amount ASC

-- 创建时间从新到旧
ORDER BY created_at DESC

数字升序通常从小到大,日期时间升序通常从早到晚。文本的先后关系取决于字符排序规则,而不是简单地等同于人类词典顺序。

多列排序​

多个排序项按从左到右的优先级依次生效:

SELECT order_id, status, created_at
FROM orders
ORDER BY status ASC, created_at DESC, order_id DESC;

数据库先按 status 分组排列;只有 status 相同时,才比较 created_at;前两项也相同时,再比较 order_id。

每个排序项都可以设置自己的方向:

ORDER BY category ASC, score DESC, name ASC

ASC 或 DESC 只控制紧邻的一个表达式,不能用末尾一个 DESC 同时控制前面所有列。

稳定排序与并列值​

如果所有排序键都相同,这些行之间的顺序仍然不确定:

ORDER BY created_at DESC

多张订单可能拥有相同的 created_at。要得到可重复的顺序,应补充一个唯一且方向明确的并列排序键:

ORDER BY created_at DESC, order_id DESC

稳定排序对以下场景尤其重要:

  • 分页列表;
  • “第一条”或“最后一条”记录;
  • 自动化测试与结果对比;
  • 数据导出和增量同步;
  • 窗口函数的顺序计算。

唯一键不一定必须单独出现,但整个排序键组合应能唯一确定每一行。

按表达式排序​

ORDER BY 可以使用输入列,也可以使用表达式:

SELECT product_id, product_name, price, discount_rate
FROM products
ORDER BY price * (1 - COALESCE(discount_rate, 0)) ASC;

还可以通过 CASE 实现业务自定义顺序:

SELECT order_id, status, created_at
FROM orders
ORDER BY
CASE status
WHEN 'pending' THEN 1
WHEN 'paid' THEN 2
WHEN 'shipped' THEN 3
WHEN 'cancelled' THEN 4
ELSE 5
END,
created_at DESC,
order_id DESC;

复杂表达式会降低可读性,也可能无法直接利用普通索引。若这种业务顺序被频繁使用,可以评估状态优先级列、生成列或表达式索引,但具体能力取决于数据库。

按输出别名排序​

ORDER BY 通常可以引用选择列表中的输出别名:

SELECT
order_id,
unit_price * quantity AS amount
FROM order_items
ORDER BY amount DESC, order_id;

这能避免重复书写复杂表达式。不过,若别名与输入列同名,名称解析可能产生歧义或数据库差异。应使用不冲突的别名,必要时增加查询层。

集合运算后的 ORDER BY 应引用整个组合结果的输出列,而不是某个分支内部才可见的表别名:

SELECT customer_id AS id, created_at
FROM current_orders
UNION ALL
SELECT customer_id, created_at
FROM archived_orders
ORDER BY created_at DESC, id;

按列序号排序​

许多数据库允许用选择列表中的位置编号排序:

SELECT customer_id, created_at, total_amount
FROM orders
ORDER BY 2 DESC, 1 ASC;

这里的 2 表示第二个输出列 created_at,1 表示第一个输出列 customer_id。这种写法简短,在临时查询或集合运算中偶尔有用,但不适合长期维护:调整选择列表顺序后,排序含义会悄悄变化。

生产查询优先使用清楚的列名或别名。

NULL 排在什么位置​

NULL 表示缺失或未知,无法按普通大小关系比较。不同数据库对升序、降序时的默认空值位置并不一致,因此需要确定性时应显式指定规则。

支持标准语法的数据库可以写:

ORDER BY shipped_at ASC NULLS LAST

或:

ORDER BY shipped_at DESC NULLS FIRST

NULLS FIRST 和 NULLS LAST 控制当前排序项的空值位置。不是所有数据库都支持这组语法,可使用 CASE 构造可移植的显式规则:

ORDER BY
CASE WHEN shipped_at IS NULL THEN 1 ELSE 0 END,
shipped_at ASC,
order_id ASC;

这会把非空时间排在前面,把未发货的空值排在最后。不要依赖从另一个数据库记住的默认行为。

文本排序与字符排序规则​

文本顺序受字符排序规则(collation)影响。它可能决定:

  • 是否区分大小写;
  • 是否区分重音符号;
  • 中文、英文、数字和标点的排列方式;
  • 字符串比较是否按语言习惯处理;
  • 索引能否直接支持该排序。

部分数据库允许为排序表达式指定字符排序规则:

SELECT name
FROM customers
ORDER BY name COLLATE some_collation;

具体规则名称和语法高度依赖数据库、操作系统及安装配置。面向用户的名称排序、大小写不敏感搜索和跨环境一致导出,都应明确验证实际排序规则。

排序发生在结果形成之后​

逻辑上,ORDER BY 晚于 FROM、WHERE、GROUP BY、HAVING 和选择列表。因此它可以按聚合结果排序:

SELECT customer_id, SUM(total_amount) AS paid_amount
FROM orders
WHERE status = 'paid'
GROUP BY customer_id
HAVING SUM(total_amount) >= 1000
ORDER BY paid_amount DESC, customer_id;

它也可以按未显示的输入列排序,许多数据库允许下面的写法:

SELECT order_id, total_amount
FROM orders
ORDER BY created_at DESC;

但在 DISTINCT 或集合运算中,排序表达式通常受到更严格限制,因为未输出的列可能无法为去重后的行确定唯一值。为了可读性与兼容性,这类查询应把必要的排序键纳入输出,或增加一层查询。

子查询中的排序​

派生表内部的 ORDER BY 不保证外层结果顺序:

SELECT *
FROM (
SELECT order_id, created_at
FROM orders
ORDER BY created_at DESC
) AS sorted_orders;

最终结果仍应由最外层 ORDER BY 决定。内部排序只有与 LIMIT、FETCH、窗口函数或数据库特定功能结合,用于确定“选哪些行”时才通常具有明确意义:

SELECT order_id, created_at
FROM (
SELECT order_id, created_at
FROM orders
ORDER BY created_at DESC, order_id DESC
FETCH FIRST 10 ROWS ONLY
) AS recent_orders
ORDER BY created_at ASC, order_id ASC;

内层选出最新 10 条,外层再按时间从早到晚展示这 10 条。

排序与索引​

排序可能需要读取全部候选行,在内存中排序,并在数据较大时写入临时磁盘。合适的索引有机会直接提供所需顺序:

WHERE customer_id = :customer_id
ORDER BY created_at DESC, order_id DESC

这类查询可能受益于以 customer_id 开头、后接排序列的复合索引。但是否使用索引取决于过滤条件、方向、空值规则、字符排序规则、返回比例和优化器成本估算。

索引设计不能只看 ORDER BY:

  1. 先确认高频查询的过滤条件;
  2. 再确认完整排序键及方向;
  3. 使用 EXPLAIN 检查实际计划;
  4. 衡量索引空间和写入维护成本。

在很小的结果集上,显式排序可能比读取索引更便宜,看到排序操作不一定代表性能问题。

随机排序要谨慎​

一些数据库允许:

ORDER BY RANDOM()

或使用其他随机函数。它通常需要为大量候选行生成随机值并整体排序,在大表上成本很高。抽样、随机推荐或随机取一行应优先研究目标数据库的采样能力、预计算随机键或适合业务分布的算法。

常见错误​

把当前返回顺序当作默认顺序​

没有 ORDER BY 就没有顺序保证。主键、聚簇存储或某个执行计划带来的顺序都不能作为接口契约。

分页时排序键不唯一​

只按非唯一时间排序,会让并列记录在页面之间跳动或重复。补充唯一键,并让游标条件与排序方向完全一致。

忽略空值和字符排序规则​

不同数据库或环境可能得到不同顺序。跨产品迁移、用户名称排序和导出任务应显式验证。

误以为内层排序会传递到外层​

每个查询层只保证自己的最终 ORDER BY。外层需要顺序时必须再次声明。

用列序号隐藏真实意图​

ORDER BY 3 对临时查询方便,但会增加维护风险。稳定代码应使用名称。

检查清单​

  1. 业务是否真正需要顺序;若需要,是否明确写了 ORDER BY;
  2. 多列优先级与每列方向是否正确;
  3. 排序键组合能否唯一确定一行;
  4. NULL 的位置是否明确;
  5. 文本排序规则是否符合语言和大小写需求;
  6. 最终排序是否写在最外层查询;
  7. 大数据量排序是否检查了执行计划与索引;
  8. 排序规则是否与分页游标条件保持一致。

可靠排序不只是添加一个列名。明确方向、并列规则、空值位置和字符排序规则,才能让结果在数据变化与多次执行之间保持可解释。