LIMIT OFFSET 深度分页性能差的本质是MySQL必须扫描并丢弃前offset行,时间复杂度O(offset+limit),无法通过索引跳过计数过程;优化需绕开OFFSET,改用游标分页(如WHERE id > last_id ORDER BY id LIMIT size),前提是排序字段有索引且支持顺序翻页。

LIMIT OFFSET 在大数据量下无法实现真正“优化”,只能被绕开——它天生不适合深度分页。 你不是写错了 SQL,也不是索引没建好,而是 MySQL 必须扫描并丢弃前 OFFSET 行,这个逻辑无法跳过。100 万行偏移?它就得读 1000020 行,再扔掉 100 万行。这不是调参能救的。
为什么 LIMIT OFFSET 深度分页会越来越慢
MySQL 不是“定位到第 N 行”,而是老老实实按 ORDER BY 排序后,从头开始逐行计数、丢弃,直到第 OFFSET + 1 行才开始取数据。
- EXPLAIN 显示
rows高达几十万甚至上百万,Extra出现Using filesort或Using index但查询仍卡住 - 第 1 页(
LIMIT 20 OFFSET 0)毫秒级,第 5000 页(LIMIT 20 OFFSET 99980)直接超时 - 加了
WHERE type = ?也无效——过滤发生在排序和偏移之后,无效扫描照旧 - 哪怕主键有索引,B+ 树也要遍历大量叶子节点才能“数够”偏移量
用 WHERE + 上一页末尾值替代 OFFSET(游标分页)
把“跳过 N 行”变成“从某个确定值之后取数据”,彻底避开偏移扫描。前提是排序字段有索引、值可比较(如 id、updated_at),且业务允许顺序翻页(不支持随机跳转)。
-
id升序分页:上一页最后一条id是6800000,下一页写SELECT * FROM t WHERE id > 6800000 ORDER BY id LIMIT 20 - 时间倒序分页(
ORDER BY updated_at DESC):上一页最后一条updated_at是'2026-04-10 15:22:33',下一页写SELECT * FROM t WHERE updated_at - 若
updated_at不唯一(多条同秒),必须叠加主键防漏/重:WHERE updated_at -
ORDER BY字段必须建索引;禁止写ORDER BY DATE(created_at)这类函数表达式,否则索引失效
实在要支持任意页码跳转?用覆盖索引 + JOIN 回表
当后台管理必须输入“第 892 页”时,优先让 MySQL 只扫描索引,不查全表,大幅减少回表次数。
- 第一步只查
id(走主键或二级索引):SELECT id FROM t WHERE type = 8 ORDER BY id LIMIT 1000000, 20(毫秒级) - 第二步用这些
id关联原表:SELECT a.* FROM t a JOIN (SELECT id FROM t WHERE type = 8 ORDER BY id LIMIT 1000000, 20) b ON a.id = b.id - 子查询中严禁写
SELECT *;主表a.id必须有索引(主键天然满足) - 实测从 37 秒降到 0.3 秒——关键在避免大偏移量下的全行回表
最容易被忽略的细节
游标分页上线后常因三个点翻车:
- 前端传错上一页末尾值:类型不对(字符串传成数字)、时区未统一(
updated_at是 UTC 却按本地时间解析)、精度丢失(毫秒级时间截断成秒) -
ORDER BY字段含NULL值:WHERE id > ?会跳过所有NULL行,需显式处理,例如WHERE (id > ?) OR (id IS NULL AND ...) - 主键非自增整型:用 UUID 或随机字符串作主键时,B+ 树分裂频繁,范围查询效率骤降,游标分页效果大打折扣


















