ROW_NUMBER()分页更可控因按逻辑序号过滤、不依赖物理偏移,避免OFFSET深分页扫描开销及重复排序值导致的漏行或重复;必须用ORDER BY+唯一列兜底、外层WHERE用BETWEEN、别名rn不可省。
因为 ROW_NUMBER() 在排序后为每一行分配唯一序号,分页逻辑完全由你自己定义——不依赖数据库执行计划对偏移量的估算,避免 OFFSET 10000 LIMIT 20 那种“先跳过一万行再取20行”的低效扫描。尤其在深分页或排序字段存在重复值时,ROW_NUMBER() 能保证结果稳定,而 OFFSET 可能因索引顺序微调导致漏行或重复。
核心是把 ROW_NUMBER() 放进子查询或 CTE,再在外层过滤序号范围。必须注意三点:
id),否则相同排序值会触发非确定性编号,导致同一页数据每次查询不一致ORDER BY 子句在 ROW_NUMBER() 内部和外部必须完全一致,否则编号顺序与最终输出顺序可能错位WHERE 中直接对 ROW_NUMBER() 别名做条件——它不能在同级查询中被引用,必须套一层子查询正确写法示例(PostgreSQL/SQL Server):
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (ORDER BY created_at DESC, id DESC) AS rn FROM posts WHERE status = 'published') tWHERE t.rn BETWEEN 21 AND 40;
MySQL 8.0+ 原生支持窗口函数,写法同上;但 MySQL 5.7 或更早版本不支持 ROW_NUMBER(),强行模拟会导致性能灾难(比如用变量自增 + 多次排序)。如果无法升级,建议:
WHERE created_at ),前提是排序字段有索引且业务允许“下一页”模式
即使给 ORDER BY 字段建了索引,ROW_NUMBER() 查询仍可能全表扫描,原因通常是:
ORDER BY created_at 有索引,但 WHERE status = 'published' 没覆盖)created_at 是 TIMESTAMP,但索引建在 DATETIME 上)ORDER BY DATE(created_at)),使索引失效验证方式:执行 EXPLAIN,重点看 key 是否命中预期索引、rows 是否接近总行数。
实际部署时,最易被忽略的是排序字段的唯一性补全——哪怕业务上认为 created_at 不会重复,也要显式加上 id 或其他主键字段,否则生产环境一旦出现毫秒级时间重复,分页就会错乱。