传统OFFSET分页性能差,因需扫描并丢弃前N行;游标分页通过记录上一页末尾排序键值实现高效翻页,要求排序字段有索引且尽量唯一。

因为传统 OFFSET 分页在数据量大时需要跳过大量已扫描的行,数据库仍要逐行遍历前 N 条记录,导致 I/O 和 CPU 开销随页码增大而线性上升。
OFFSET 越大,性能越差
比如 SELECT * FROM orders ORDER BY id LIMIT 20 OFFSET 100000,MySQL 或 PostgreSQL 仍需先定位到第 100001 行——这意味着它必须读取并丢弃前 10 万行(即使只返回 20 行)。索引虽能加速排序,但 OFFSET 本身无法跳过中间数据,全靠顺序扫描或索引跳跃模拟,效率天然受限。
- 每翻一页,数据库重复执行一次“找起点”的动作,不是缓存位置而是重算偏移
- 如果排序字段有重复值,OFFSET 还可能因排序不唯一导致分页错位(如两次查询同一页结果不一致)
- 高并发下,大 OFFSET 查询容易持有锁更久、加剧 MVCC 版本链遍历开销(尤其 PostgreSQL)
用游标分页(Cursor-based Pagination)替代 OFFSET
核心是“记住上一页最后一条的排序键值”,下一页直接从该值之后查起,避免跳过历史数据。例如按 id 升序分页:
- 第一页:
SELECT * FROM orders ORDER BY id LIMIT 20,记下最后一条的id = 12345 - 第二页:
SELECT * FROM orders WHERE id > 12345 ORDER BY id LIMIT 20 - 要求排序字段(如 id)必须有索引、且尽量唯一;若复合排序(如
created_at, id),条件也要对应写全:WHERE (created_at, id) > ('2024-01-01', 999)
覆盖索引 + 延迟关联优化大偏移查询
如果暂时无法改游标,可减少回表和传输开销:
- 先用覆盖索引查出主键(如
SELECT id FROM orders ORDER BY id LIMIT 20 OFFSET 100000),该步骤仅走索引树,快很多 - 再用这些 id 关联原表查完整字段:
SELECT o.* FROM orders o JOIN (子查询) t ON o.id = t.id - 适合 MySQL 场景,但不如游标分页彻底,仍受 OFFSET 本质限制
其他实用建议
提前拦截不合理请求:比如限制最大页码(page ≤ 1000)、或前端禁用“跳转到末页”按钮;对实时性要求不高的列表,可预生成分页摘要(如按时间归档的月度汇总页);监控慢查询日志中 LIMIT … OFFSET [大数] 模式,主动推动改造。










