row_number()分页不比limit快,因其本身仅打标,真正提速需排序走索引、中间集可控、过滤尽早发生;错误用法如无索引order by、where中直接引用rn、未含主键导致序号不稳定、窗口外未过滤等会严重拖慢性能。

为什么ROW_NUMBER()分页不比LIMIT快?
它本身不快,快的是你用对了方式。ROW_NUMBER()只是给每行打标,真正提速靠的是避免OFFSET逐行跳过——但前提是排序走索引、中间结果集不爆炸、别在WHERE里直接引用ROW_NUMBER()。
- EXPLAIN里看到
Using temporary; Using filesort,说明ORDER BY字段没索引,窗口函数白搭 -
WHERE rn > 1000000 LIMIT 20写法错误:MySQL不允许在同级WHERE中引用窗口别名,必须套CTE或子查询 - 没加主键到ORDER BY里(比如只写
ORDER BY create_time DESC),时间相同会导致rn分配不稳定,翻页重复或漏数据 - WHERE条件写在窗口外(如CTE里过滤status='done'),否则窗口会先算全表再过滤,内存压力翻倍
怎么写才能真提速?
核心是让排序可下推、中间集可控、过滤尽早发生。不是套个ROW_NUMBER()就完事。
- 用CTE结构:
WITH ranked AS ( SELECT id, name, create_time, ROW_NUMBER() OVER (ORDER BY create_time DESC, id ASC) AS rn FROM orders WHERE status = 'shipped' ) SELECT id, name, create_time FROM ranked WHERE rn BETWEEN 100001 AND 100020; - ORDER BY必须含索引列组合:建复合索引
INDEX (status, create_time, id),让WHERE+ORDER BY一起走索引 - 别用
PARTITION BY做普通分页——它会让ROW_NUMBER()按组重编号,全局序号就乱了 - 页码参数要转成BETWEEN范围,注意整数溢出:
($page - 1) * $size + 1可能超BIGINT,尤其$size=10000、$page=100000时
什么情况下它反而更慢?
不是所有分页都适合换。当你的瓶颈不在OFFSET,而在排序本身时,ROW_NUMBER()只会雪上加霜。
- 数据量小(LIMIT 1000, 20比CTE+窗口快得多,少一次全排序
- ORDER BY字段无索引:引擎被迫磁盘排序,
Using filesort耗时远超OFFSET扫描 - 内存受限(如RDS 2GB规格):千万行打标需要缓存全部排序结果,OOM风险高
- 实时性要求极高:窗口基于事务快照,两次查询间若新插入数据,
rn会变,但页面内容可能“跳帧”
游标分页才是千万级的正解
真要撑住千万级,别纠结ROW_NUMBER()和LIMIT谁更快——它们都扛不住随机跳页。该换思路。
- 记录上一页最后一条的
(create_time, id),下一页用:WHERE (create_time, id) > ('2025-06-01', 9999999) ORDER BY create_time DESC, id ASC LIMIT 20 - 这个WHERE能走联合索引,完全避开排序和打标开销
- 后台管理需要跳第1000页?预生成锚点表:每10000行存一次首个
id,查页时先查锚点再LIMIT - MySQL 8.0的
ROW_NUMBER()仍是伪窗口函数,底层依赖临时表;别把它当PostgreSQL或SQL Server那样信任执行计划
实际线上跑千万级订单表,游标分页QPS稳定在1200+,而ROW_NUMBER()分页在第500页后开始抖动,EXPLAIN FORMAT=TREE里WindowAgg节点占比超70%——这时候再调索引也没用,得切访问模式。











