row_number()性能差主因是索引不匹配,必须按partition by和order by字段顺序、方向建复合索引,如(dept, salary desc),where条件字段(如status)应置键列而非include,空order by或函数包裹排序列将导致全表扫描与隐式排序。

ROW_NUMBER() 性能差,八成是索引没对上,不是数据量大。 它不走普通查询索引,必须按 PARTITION BY 和 ORDER BY 字段顺序、方向、完整性重建复合索引,否则每次执行都触发全表扫描 + 内存排序。
索引字段顺序必须严格匹配 OVER 子句
SQL Server 执行 ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) 时,需要先按 dept_id 切分数据块,再在每个块内按 salary DESC 排序。如果索引是 (salary DESC, dept_id),它无法跳过排序;只有 (dept_id, salary DESC) 才能直接复用物理顺序。
-
PARTITION BY字段必须放在索引最左侧(最左前缀原则) -
ORDER BY字段紧随其后,且ASC/DESC必须完全一致(salary DESC对应索引里也要写DESC) - WHERE 条件字段(如
status = 1)建议也前置进索引键列,而非只放INCLUDE,否则仍需回表过滤
覆盖索引要包含 SELECT 列和计算所需字段
理想索引不仅要支撑窗口计算,还要让 SQL Server 不查数据页就能返回结果。比如:
SELECT id, name, dept, salary, ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn FROM emp WHERE status = 1;
对应索引应为:
CREATE NONCLUSTERED INDEX idx_dept_salary_status_cover ON emp (dept, salary DESC, status) INCLUDE (id, name, salary);
-
dept和salary DESC对齐窗口逻辑 -
status放键列(非INCLUDE),才能高效过滤WHERE status = 1 -
salary已在键列中,INCLUDE里不必重复加;id、name是 SELECT 需要但非排序/分区字段,放INCLUDE最省空间
空 ORDER BY 或函数包裹排序字段会彻底失效
ROW_NUMBER() OVER (PARTITION BY dept) 看似合法,实则触发隐式全表排序——SQL Server 按任意物理顺序编号,结果不可控,且优化器常退化为堆扫描。
- 所有排名函数(
ROW_NUMBER、RANK、DENSE_RANK)必须显式写ORDER BY,否则就是性能炸弹 -
ORDER BY UPPER(name)、ORDER BY YEAR(created_at)这类写法会让索引完全失效,强制文件排序 -
COUNT(*) OVER ()这种空括号写法虽不报错,但会强制全局排序,尤其在无聚集索引的表上开销极大
别只盯着索引,先确认是否真需要 ROW_NUMBER()
很多场景本质不是“编号”,而是“取每组最新一条”或“分页第 N 页”。这时 ROW_NUMBER() 是过度计算:
- 分组取最新:用
OUTER APPLY (SELECT TOP 1 ... ORDER BY updated_at DESC),避免对全表做分组排序 - 深度分页(如第 10 万页):游标分页(
WHERE (updated_at, id) > (@last_time, @last_id))比WHERE rn BETWEEN 999991 AND 1000000快一个数量级 - 仅需累计值:
SUM(salary) OVER (PARTITION BY dept ORDER BY hire_date)可以不建索引,但若后续要取“部门内薪资排名前 3”,就必须有(dept, hire_date)索引
真正容易被忽略的是:索引建完必须验证执行计划里是否出现 Index Seek 而非 Index Scan 或 Sort;只要看到 Sort 算子,说明索引还是没对上。










