UNION后外层ORDER BY不走索引,因UNION先执行生成无序临时表,外层排序面对的是无索引结果集;子查询内ORDER BY需配合LIMIT才生效,且所有子查询与外层ORDER BY字段、方向必须严格一致才能复用索引。

MySQL中UNION后ORDER BY不走索引的根源
直接在UNION语句外层加ORDER BY,看似合理,但往往查得慢、执行计划里type还是ALL——不是语法错了,而是MySQL根本没机会用上子查询里的索引排序。因为UNION本身在执行顺序中位于ORDER BY之前(准确说:SELECT → DISTINCT → UNION → ORDER BY),外层ORDER BY面对的是已经拼好的无序临时结果集,索引早失效了。
为什么“外层包装查询”常被误用
很多人写成这样:
(SELECT * FROM t1 WHERE a = 1 ORDER BY b) UNION ALL (SELECT * FROM t2 WHERE c = 2 ORDER BY d)
然后在外面再套一层:
SELECT * FROM (上面那个UNION) AS tmp ORDER BY x;
这仍然不会让b或d的索引生效。原因有二:
-
ORDER BY b在子查询里被MySQL忽略(除非加LIMIT) - 外层
ORDER BY x作用对象是临时表tmp,而tmp没有索引,只能全表排序
换句话说:包装本身不解决索引问题,只是把排序延迟到更晚阶段,代价反而更高。
真正能利用索引的外层包装写法
关键不是“包不包装”,而是“包装后是否保留可下推的排序意图”。可行路径只有一条:让每个子查询输出时已按目标字段有序,且外层ORDER BY能复用该顺序(例如按同一字段升序合并)。实操要点:
- 每个子查询必须显式加
LIMIT(哪怕设为大数),否则内部ORDER BY被优化器跳过 - 所有子查询的
ORDER BY字段需一致(如都按create_time DESC),且该字段上有索引 - 外层
ORDER BY必须与子查询一致(不能一个DESC一个ASC),否则仍触发临时文件排序 - 避免
SELECT *,只选必要字段,减少临时表体积
示例(假设create_time有索引):
(SELECT id, title, create_time FROM news WHERE status = 1 ORDER BY create_time DESC LIMIT 10000) UNION ALL (SELECT id, title, create_time FROM articles WHERE status = 1 ORDER BY create_time DESC LIMIT 10000) ORDER BY create_time DESC;
此时执行计划中Extra可能出现Using filesort,但至少子查询部分走了索引范围扫描,比全表拼接后再排序快得多。
比外层包装更稳的替代方案
如果业务允许,优先考虑以下两种方式,它们天然规避UNION排序陷阱:
-
用
UNION ALL+ 辅助排序字段:给每个子查询加一个seq列(如1 AS seq,2 AS seq),外层先按seq再按业务字段排序。这样MySQL能用seq快速分组,再对每组内字段做局部索引扫描 -
改用
JOIN或应用层合并:当两个数据源结构高度一致、且数量可控时,查两次+内存归并,比硬扛MySQL的UNION排序更可控、更易调试
最易被忽略的一点:即使加了LIMIT和统一ORDER BY,只要子查询返回行数超过sort_buffer_size,MySQL仍会退化为磁盘排序。线上务必检查SHOW STATUS LIKE 'Sort_merge_passes';,值持续上升就是信号。











