offset分页性能差因需逐行扫描跳过数据,游标分页通过where条件实现恒定时间查询,延迟关联则先查id再join减少回表。

因为 OFFSET 不是“跳转”,而是“逐行扫描并丢弃”——数据库必须读取前 OFFSET + LIMIT 行,哪怕你只要最后 20 条。
OFFSET 越大,MySQL 实际扫描的行数越多
执行 LIMIT 1000000, 20 时,MySQL 并不会直接定位到第 1000001 行。它得从索引头开始,一条条往下扫、计数、跳过前 100 万行,再取后面 20 条。这些被跳过的行照样触发 I/O、回表、排序判断,甚至写磁盘临时文件。
- EXPLAIN 中
rows值随 OFFSET 线性增长,不是错觉,是真实扫描量 - 如果
ORDER BY created_at字段有重复值,优化器更可能放弃索引范围扫描,退化为全索引遍历 -
SELECT *配合非覆盖索引时,每跳过一行都可能引发一次随机回表,I/O 雪崩
为什么加索引也救不了大 OFFSET
索引能加速排序和定位,但不能绕过“跳过前 N 行”这个动作。B+ 树仍需逐层遍历大量节点来完成计数,尤其当 created_at 重复率高、或联合索引没对齐查询条件时,索引效率断崖下跌。
- 复合索引
(created_at, id)有用,但只在WHERE和ORDER BY完全匹配时生效 - 如果查询带
WHERE status = 'paid'却只建了(created_at)索引,大概率走全表扫描 - MySQL 8.0 仍未优化 OFFSET 逻辑;PostgreSQL 13+ 有少量改进,但不解决根本问题
游标分页(WHERE id > last_id)为什么快
它把“我要第 N 页”变成“我要比上一页最后一条更新的所有数据”,彻底消除跳过动作,每次都是索引范围扫描,执行时间恒定。
- 第一句:
SELECT * FROM orders WHERE status = 'paid' ORDER BY id LIMIT 20 - 拿到最后一行
id = 6800000后,第二句写成:SELECT * FROM orders WHERE status = 'paid' AND id > 6800000 ORDER BY id LIMIT 20 - 若按时间分页且
created_at可能重复,必须补主键:ORDER BY created_at DESC, id DESC,WHERE 条件也要写成WHERE (created_at, id)
延迟关联(Deferred Join)适合还必须跳页的场景
后台管理系统要支持输入任意页码,又没法改成分页模式?那就用子查询先捞 ID,再 JOIN 查全字段,把回表控制在 20 次以内。
- 原语句慢:
SELECT * FROM articles WHERE category_id = 5 ORDER BY created_at DESC LIMIT 10000, 20 - 优化后:
SELECT a.* FROM articles a INNER JOIN (SELECT id FROM articles WHERE category_id = 5 ORDER BY created_at DESC LIMIT 10000, 20) t ON a.id = t.id - 前提:必须有联合索引
(category_id, created_at, id),子查询只查id,否则优化失效
真正容易被忽略的是:游标分页要求前端完整保留上一页末尾的排序键(不止一个字段),传参漏掉 id 或类型不对(比如把 int 当 string 传),结果就可能漏数据或重复。这不是后端能兜住的问题。











