ROW_NUMBER() 本身不慢,慢是因为它被迫在没索引的中间结果上全量排序;多表关联导致数据膨胀、ORDER BY 字段无索引触发磁盘排序、NULL 值干扰顺序、索引未匹配窗口排序字段及顺序、函数表达式使索引失效、并行窗口粒度失配、视图中滥用 ROW_NUMBER() 阻碍优化器下推。

ROW_NUMBER() 本身不慢,慢是因为它被迫在没索引的中间结果上全量排序。
为什么 JOIN 后加 ROW_NUMBER() 就卡住
多表关联后数据集膨胀,比如 orders × users × products 联合结果可能从百万级变成千万级。ROW_NUMBER() OVER (ORDER BY ...) 必须等整个关联结果物化完成,再全局排序编号——此时若 ORDER BY 字段没索引,就会触发磁盘 external merge 排序,I/O 直接拉垮。
- 用
EXPLAIN ANALYZE看执行计划,如果WindowAgg节点出现在Merge Join或Hash Join之后,且Sort Method显示external merge,就是典型征兆 - 排序字段尽量选驱动表(如主表
orders.created_at),别用从表计算字段(如users.name || products.title) - LEFT JOIN 引入大量
NULL时,ORDER BY默认把NULL排最前/最后,导致编号顺序错乱,甚至让优化器放弃索引
索引建了,但 ROW_NUMBER() 还是不走
窗口函数不直接“走索引”,它依赖物理扫描阶段能否利用索引有序性。一旦关联复杂,优化器常选哈希连接 + 后排序,绕过索引。
- 必须建联合索引,且字段顺序严格匹配
OVER (ORDER BY a, b DESC)→ 索引要写成CREATE INDEX idx_a_b ON t(a, b DESC) - 如果
WHERE常带status = 'paid',得把status放索引最左列:CREATE INDEX idx_status_a_b ON t(status, a, b DESC) -
ORDER BY里用了函数(如DATEADD(day, 1, created_at))或表达式,索引完全失效,必然回表+全排序
并行执行计划下反而更慢
并行不是万能解药。窗口函数要求“分区完成后再编号”,高基数 PARTITION BY(如百万级 user_id)会生成海量轻量排序任务,线程调度开销压倒收益;低区分度分区(如只有 'active'/'inactive')则撑爆内存,频繁溢出到磁盘。
- 看执行计划里是否出现大量
WindowAgg (parallel)节点但耗时飙升——大概率是粒度失配 - 游标分页(
WHERE (created_at, id) < (@last_time, @last_id))可彻底规避编号开销,但要求排序键唯一、非空、有覆盖索引 - 如果业务只要“每个分组最新一条”,别用
ROW_NUMBER(),改用聚合 + 关联,算子流水执行更高效
真正拖慢查询的从来不是 ROW_NUMBER() 这个函数名,而是你没把它放在索引能覆盖的路径上,也没控制好它作用的数据规模。最容易被忽略的一点:别在视图定义里直接套 ROW_NUMBER(),而要在展开后的基础表上做——否则优化器根本没法下推过滤和索引选择。

















