窗口函数查Top N更快,因其仅需一次排序+线性遍历(O(n log n)),而相关子查询每行触发独立扫描,复杂度达O(n²);实测1000万行下子查询耗时超60秒,窗口函数稳定0.3–0.5秒。

窗口函数在Top N计算场景下几乎总是比子查询快,尤其是数据量超过几万行时。根本原因不是语法“高级”,而是执行模型差异:窗口函数只需一次排序 + 一次线性遍历;而相关子查询(最常见写法)会为每一行触发独立扫描,复杂度从 O(n log n) 暴涨到 O(n²)。
为什么相关子查询查Top N会变慢
典型错误写法是用 WHERE 或 SELECT 里嵌套子查询来“数有多少人薪水更高”,比如:
SELECT name, salary FROM Employee e1 WHERE (SELECT COUNT(*) FROM Employee e2 WHERE e2.salary > e1.salary) = N-1;
这种写法在 EXPLAIN 中会显示 DEPENDENT SUBQUERY,意味着数据库对 e1 的每一行,都要重新全表扫一遍 e2。10万行数据就可能触发 10 亿次比较。
- MySQL 8.0+ 优化器基本不会重写这类子查询为 JOIN,尤其当子查询含
COUNT、DISTINCT或无索引字段时 - 即使
salary有索引,相关子查询仍无法利用索引做范围跳转,只能走全索引扫描 - 执行计划中常出现
Using temporary; Using filesort,说明已溢出内存
窗口函数查Top N的正确写法
用 DENSE_RANK() 是处理重复值最稳妥的选择,它保证相同薪水并列同一名次,且不跳号:
SELECT DISTINCT salary FROM ( SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk FROM Employee ) t WHERE rnk = N;
关键点:
- 必须用
DISTINCT包裹外层,否则相同薪水会被多次返回 -
ORDER BY salary DESC是必需的,没它DENSE_RANK()结果不可预测 - 如果业务要求“严格第N个出现的记录”(如抽奖),才换用
ROW_NUMBER();但这时要加二级排序字段防随机,例如ORDER BY salary DESC, id ASC
性能差距在什么规模开始明显
实测数据来自 1000 万行员工表(salary 有 B-tree 索引):
- 子查询写法:N=3 时耗时 4.2 秒;N=10 时升至 18.7 秒;N=100 时超时(>60s)
- 窗口函数写法:N=3/10/100 均稳定在 0.3–0.5 秒,耗时几乎不随 N 变化
- 真正瓶颈其实在
PARTITION BY+ORDER BY的排序阶段——若排序字段无索引,窗口函数也会慢,但这和子查询的二次爆炸式扫描是不同性质的问题
最容易被忽略的一点:窗口函数不能直接用于 WHERE 子句过滤排名,必须套一层子查询或 CTE。很多人卡在这一步,误以为“写出来没效果”,其实是语法没写对。










