mysql 8.0+ 应直接用 row_number() 实现行序号,必须配合 over(order by ...),缺 order by 会报错;分页不推荐用 row_number() 模拟,因需全表排序编号,性能反不如 limit。

MySQL 8.0+ 直接用 ROW_NUMBER() 最省事
如果你用的是 MySQL 8.0 或更高版本,根本不用写嵌套子查询来分页——ROW_NUMBER() 加 OVER() 就能干净解决。老式 LIMIT offset, size 在大表偏移量大时会全表扫描,而窗口函数可以配合索引高效定位。
常见错误是直接套用 PostgreSQL 写法,比如漏写 ORDER BY:窗口函数要求排序确定性,否则 ROW_NUMBER() 结果不稳定,分页会跳行或重复。
-
ORDER BY必须存在,且最好基于有索引的列(如id或created_at) - 子查询里不能用
SELECT *配合ROW_NUMBER(),否则可能因字段歧义报错;显式列出字段更安全 - 示例:
SELECT * FROM (SELECT id, name, ROW_NUMBER() OVER (ORDER BY id) AS rn FROM users) t WHERE t.rn BETWEEN 21 AND 30;
MySQL 5.7 及更早版本必须靠子查询模拟行号
没有窗口函数,就得用变量或自连接生成序号。但变量方式(@row := @row + 1)在多线程或优化器重排下不可靠,官方已明确不保证执行顺序;真正稳定的做法是用「相关子查询」数前几行个数,本质是为每行算“前面有多少行”。
性能是最大陷阱:这种写法时间复杂度接近 O(n²),10 万行以上基本卡死。只适合小数据集或临时导出。
- 必须确保外层
ORDER BY和子查询里的排序一致,否则序号乱序 - 子查询中不能有
GROUP BY或聚合,否则相关性被破坏 - 示例(查第 3 页,每页 10 条):
SELECT u1.id, u1.name FROM users u1 WHERE (SELECT COUNT(*) FROM users u2 WHERE u2.id
PostgreSQL / SQL Server 用 OFFSET 要小心性能退化
OFFSET 看似简单,但数据库仍要扫描并跳过前 N 行。当 OFFSET 100000 时,即使有索引,I/O 和 CPU 开销也陡增。这不是语法问题,而是执行计划本质决定的。
替代方案不是换子查询,而是用「游标分页」(cursor-based pagination):记录上一页最后一条的排序键值,下一页查 WHERE sort_key > last_value。这跳过了跳过动作,索引可直奔目标位置。
- 不能用
OFFSET做无限滚动加载(如微博下拉),尤其高并发场景 -
OFFSET和LIMIT组合在ORDER BY列无索引时,会触发 filesort,慢得离谱 - 如果非要用子查询包装
OFFSET(比如套一层SELECT * FROM (…) t),除了增加解析开销,没实际好处
子查询分页在 JOIN 场景下极易翻车
一旦主表关联其他表(比如 users JOIN orders),子查询里没处理好去重或聚合,分页结果就会重复或漏行。核心问题是:分页该在 JOIN 前还是后做?
正确做法永远是先分页再 JOIN——即子查询只查主表 ID,外层再用这些 ID 关联详情。否则,一对多关系会让子查询结果膨胀,LIMIT 截断的是连接后的行,不是原始用户数。
- 错误写法:
SELECT u.*, o.amount FROM users u LEFT JOIN orders o ON u.id = o.user_id LIMIT 10 OFFSET 20→ 可能只返回 3 个用户(因为每个用户有多条订单) - 正确写法:先用子查询取 10 个
user_id,再JOIN订单表,避免数量失真 - 示例:
SELECT u.*, o.amount FROM (SELECT id FROM users ORDER BY id LIMIT 10 OFFSET 20) page JOIN users u ON u.id = page.id LEFT JOIN orders o ON o.user_id = u.id;
子查询分页不是语法技巧问题,本质是数据规模、索引设计和执行引擎限制共同作用的结果。最容易被忽略的,是把“能跑通”当成“能上线”——测试数据 1000 行没问题,上线后百万行就超时,往往就栽在这一步。











