SQL 分页查询优化策略与实践

1次阅读

mysql limit offset 越往后越慢是因为需先跳过offset行再取数据,导致全表扫描;应改用游标分页,以排序字段值为起点避免跳过大量数据。

SQL 分页查询优化策略与实践

MySQL LIMIT OFFSET 为什么越往后越慢

因为数据库必须先扫描并跳过前 OFFSET 行,再取 LIMIT 行——数据量大时,跳过几十万行就是全表扫描的代价。不是“取10条慢”,而是“跳过999990条”慢。

  • 真实场景:用户翻到第1000页(OFFSET 99990),哪怕有索引,MySQL 仍需定位到第99990个索引位置,逐条计数推进
  • 复合索引有效但有前提:必须覆盖 ORDER BY 字段 + 查询字段,且 WHERE 条件能命中索引最左前缀
  • 如果 ORDER BY id DESC,但 id 是主键,LIMIT 10 OFFSET 100000 依然慢——InnoDB 的聚簇索引遍历无法跳过中间节点
  • EXPLAINrows 值,如果远大于 LIMIT 数,基本就是 OFFSET 陷阱

用游标分页(Cursor-based Pagination)替代 OFFSET

不依赖行号,改用上一页最后一条记录的排序字段值作为下一页起点,避免跳过大量数据。

  • 适用前提:排序字段必须唯一、非空、有索引(如 created_at + id 组合,或直接用主键 id
  • 查询写法:把 WHERE id > ? ORDER BY id ASC LIMIT 10 替代 LIMIT 10 OFFSET 1000
  • 注意方向一致性:如果前端按时间倒序展示,后端应存上一页最小的 created_at,再查 WHERE created_at
  • 不能直接用于“跳转任意页”,只适合“下一页/上一页”连续浏览;搜索、筛选后首次加载仍需一次带 OFFSET 的查询(但仅第一次)

postgresql 的 cursor 分页更友好?

是的,但不是语法糖,而是底层支持更成熟的游标机制和更精准的索引扫描策略。

  • DECLARE my_cursor CURSOR for select ... ORDER BY id; 配合 FETCH NEXT 10 FROM my_cursor 可复用游标状态,适合长连接分批拉取
  • 更常用的是基于 WHERE id > $last_id 的无状态游标,和 MySQL 逻辑一致,但 PostgreSQL 的索引跳跃能力略强,尤其在 WHERE + ORDER BY 同字段时
  • 注意:如果排序字段有重复值(比如多个记录 created_at 相同),必须加二级排序(如 ORDER BY created_at DESC, id DESC),否则可能漏行或重复
  • PostgreSQL 的 OFFSET 同样慢,别以为换数据库就自动优化

什么时候还不得不硬扛 OFFSET?

当业务明确要求“跳转到第 N 页”且 N 不大(比如 ≤ 50),或数据总量小(LIMIT OFFSET 仍是最快落地方案。

  • 加缓存:对固定页码(如热门榜单第1页、第3页)做 redis 缓存,key 为 page:rank:1,避免反复查库
  • 限制页码上限:后端校验 page * size ,超出直接返回空或提示“已到末尾”,防止恶意刷大 offset
  • 用物化视图或汇总表预计算:比如每日凌晨跑任务,把“用户昨日活跃排名前1000”固化到 daily_rank_snapshot 表,分页查这张轻表
  • 不要在事务里嵌套深分页查询——锁范围随 OFFSET 增大而扩大,容易阻塞写操作

游标分页看着简单,但排序字段选错、重复值没兜底、前端传参校验松,三者任一出问题,分页就错位。真上线前,拿百万级测试数据跑一遍第100页、第500页的边界 case。

text=ZqhQzanResources