ROW_NUMBER()必须配合OVER子句使用,否则报错;WHERE条件需下推至子查询内以减少排序行数,ORDER BY须含唯一列兜底防抖动,索引需严格匹配排序字段顺序。

ROW_NUMBER() 必须配合 OVER 子句使用,单独写会直接报错:Window function 'ROW_NUMBER' requires an OVER clause。它不是普通函数,而是窗口函数,执行顺序决定了你不能在外层 SELECT 后直接加 WHERE rn BETWEEN——必须用子查询或 CTE 先算出行号,再过滤。
为什么子查询里必须写 WHERE 条件,不能挪到外层?
把 WHERE status = 'paid' 放在外层,等于让数据库先给全表几万行编号,再筛出其中几十条;而放在子查询内,能大幅减少参与排序和编号的行数,索引也能生效。
- 错误写法:
SELECT * FROM (SELECT *, ROW_NUMBER() OVER (ORDER BY id) AS rn FROM orders) t WHERE t.status = 'paid' AND t.rn BETWEEN 11 AND 20——rn是按全表排的,第 11 行很可能是status = 'draft' - 正确写法:
SELECT * FROM (SELECT *, ROW_NUMBER() OVER (ORDER BY id) AS rn FROM orders WHERE status = 'paid') t WHERE t.rn BETWEEN 11 AND 20 - WHERE 下推后,执行计划中
Estimated Number of Rows在ROW_NUMBER()节点附近应接近目标页大小(如 20),而非百万级
ORDER BY 怎么写才不会翻页抖动?
排序字段有重复值(比如大量 created_at 精度为秒)时,ROW_NUMBER() 分配序号可能不稳定——同一条记录在第 2 页和第 3 页都出现。这不是 bug,是排序未完全确定导致的逻辑不确定性。
- 必须补唯一列兜底,例如:
ORDER BY created_at DESC, id DESC,而不是只写ORDER BY created_at DESC - 如果排序字段允许
NULL,MySQL 要写ORDER BY IFNULL(created_at, '1970-01-01') DESC,PostgreSQL 则用ORDER BY created_at DESC NULLS LAST - 避免
ORDER BY RAND()——每次执行结果乱序,分页彻底失效
索引怎么建才能让 ROW_NUMBER() 不变慢?
ROW_NUMBER() 分页慢,90% 的原因是 ORDER BY 字段没索引,导致执行计划里出现 Sort 运算符,数据量一上万就从毫秒变秒级。
- 索引必须和
OVER (ORDER BY ...)完全对齐:若写ORDER BY status, updated_at DESC, id,就得建CREATE INDEX IX_tasks_status_time_id ON tasks(status, updated_at DESC, id) - 不要在排序字段上用表达式,例如
ORDER BY DATEADD(day, 1, created_at)—— 索引无法命中,必然触发 Sort - 关联查询时,确保 JOIN 字段也有索引,否则
ROW_NUMBER()前的中间结果集就已膨胀
CTE 和子查询,选哪个更安全?
两者性能基本一致,但 CTE 更易读、结构更清晰;不过要注意命名冲突和版本兼容性细节。
- PostgreSQL 中 CTE 名字不能和真实表名相同,否则可能报
column "rn" does not exist - SQL Server 的 CTE 有默认递归限制(100 层),虽然分页不涉及递归,但若后续叠加其他 CTE 容易意外触雷
- MySQL 8.0+ 和 PostgreSQL 都支持标准语法,但 MySQL 5.7 及更早不支持窗口函数,别踩坑
真正容易被忽略的是:深度分页(比如第 5000 页)时,ROW_NUMBER() 仍需扫描并编号前 N 行,只是比 OFFSET 更可控;如果你的业务真需要跳转到极深页码,得考虑游标分页(WHERE id )或预计算总数 + 缓存偏移映射。










