MySQL 5.6+ 中子查询的 ORDER BY 默认被优化器忽略,仅当配合 LIMIT 或用于 UNION 等特定上下文时生效;真正决定结果顺序的必须是外层 SELECT 的 ORDER BY。

子查询(派生表)里的 ORDER BY 在 MySQL 5.6+ 中默认被优化器忽略,除非它配合 LIMIT 或用于特定上下文(如 UNION 子句中)。这不是 bug,是设计行为——因为派生表本身不承诺顺序,外层查询若没再排序,结果顺序不可控。
为什么子查询的 ORDER BY 会被 MySQL 优化掉
MySQL 认为:派生表只是中间结果集,没有“行序”语义;ORDER BY 若不跟 LIMIT,对最终输出无实际影响,所以直接移除。你执行 EXPLAIN 就能看到优化后的 SQL 里 ORDER BY 已消失。
- 仅在子查询中写
ORDER BY created_at DESC→ 无效 - 子查询中写
ORDER BY created_at DESC LIMIT 10000→ 有效(必须带LIMIT) - 子查询用作
UNION ALL的一部分 → 同样被优化,除非每个分支都加LIMIT
真正生效的排序必须落在最外层 SELECT
想让结果按某字段排好序,ORDER BY 必须出现在最终返回结果的那条 SELECT 语句末尾,不能藏在子查询里。例如:
SELECT * FROM ( SELECT id, user_id, created_at FROM message ) t ORDER BY created_at DESC; -- ✅ 这里才起作用
- 即使子查询里也写了
ORDER BY,只要外层没写,顺序就不可靠 - 分页场景(
LIMIT offset, size)必须和外层ORDER BY配套,否则第一页和第二页可能重复或漏数据 - 如果外层还套了
GROUP BY,注意:MySQL 允许SELECT *+GROUP BY,但非分组字段值是随机选取的,ORDER BY无法控制它选哪一行
需要“每组取最新一条”时别依赖子查询排序
常见错误写法:SELECT * FROM (SELECT * FROM message ORDER BY created_at DESC) t GROUP BY conversation_id。这看似取每组最新,实则行为未定义,MySQL 可能返回任意一条(通常是索引扫描顺序的第一条)。
- 正确做法一:用窗口函数(MySQL 8.0+)
SELECT * FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY conversation_id ORDER BY created_at DESC) rn FROM message) t WHERE rn = 1 - 正确做法二:先聚合再 JOIN
SELECT m.* FROM message m INNER JOIN (SELECT conversation_id, MAX(created_at) max_time FROM message GROUP BY conversation_id) t ON m.conversation_id = t.conversation_id AND m.created_at = t.max_time - 正确做法三:用相关子查询(兼容老版本,但性能差)
SELECT * FROM message m1 WHERE created_at = (SELECT MAX(created_at) FROM message m2 WHERE m2.conversation_id = m1.conversation_id)
UNION ALL 场景下各分支排序仍需外层兜底
UNION ALL 各子查询的 ORDER BY 即使加了 LIMIT,也只能保证该分支内部顺序,合并后整体仍无序。例如:
(SELECT * FROM a ORDER BY ts DESC LIMIT 10) UNION ALL (SELECT * FROM b ORDER BY ts DESC LIMIT 10)
合并后的 20 条记录不保证按 ts 降序排列。
- 若要整体有序,必须在外层再套一层:
SELECT * FROM (...) t ORDER BY ts DESC - 若还要分页,
LIMIT必须放最外层,否则内层LIMIT会提前截断,导致漏数据 - 使用变量模拟序号(如
@i:=@i+1)属于 hack 手段,不推荐用于生产;MySQL 8.0 应优先用窗口函数
关键点始终只有一个:SQL 标准不承诺任何查询结果的隐式顺序。子查询里的 ORDER BY 是个幻觉,除非它服务于 LIMIT 或出现在最终输出层。别让它骗你两次。











