limit offset 在千万级表上必然变慢,因mysql需顺序扫描前offset行并丢弃,无法物理跳过;优化方案包括延迟关联(先查id再join)和游标分页(用排序字段值做边界)。

LIMIT OFFSET 在千万级表上根本不能靠“优化”来救,必须换方案。 它的性能衰减是执行模型决定的,不是索引没建好、SQL写得不够巧的问题——MySQL 真的会逐行扫描前 OFFSET 行,再丢弃。你加再多索引,也绕不开这个动作。
为什么 LIMIT OFFSET 深度分页必然变慢
这不是 MySQL 的 bug,而是 B+ 树遍历 + 执行器行为共同导致的:
- MySQL 不支持“跳过 N 行”的物理定位,
OFFSET越大,它越要从索引头开始顺序读取OFFSET + LIMIT行 - 如果
ORDER BY字段没覆盖索引,每读一行都可能触发回表(主键查找),IO 成倍放大 -
OFFSET超过 10 万后,扫描行数常达返回行数的百倍以上,CPU 和磁盘压力陡增 - 数据有增删时,
OFFSET分页还会出现漏数据或重复数据(“数据漂移”)
延迟关联(Deferred Join):最小改动、最大收益
把“查全量”拆成两步:先用覆盖索引捞 ID,再用 ID 精准回查。适用于仍需支持跳页的后台系统。
原始低效语句:SELECT * FROM t_order WHERE status = 1 ORDER BY create_time DESC LIMIT 990000, 10;
优化后写法:SELECT t1.* FROM t_order t1 INNER JOIN ( SELECT id FROM t_order WHERE status = 1 ORDER BY create_time DESC LIMIT 990000, 10 ) t2 ON t1.id = t2.id;
- 必须有联合索引,例如
idx_status_ctime(status,create_time) - 子查询只查
id,全程走索引,不回表 - 外层
JOIN只对 10 个id回表,IO 从 ~990010 次降到 10 次 - 注意:如果
t_order表用的是 UUID 主键,且id不是聚簇索引字段,性能提升会打折扣
游标分页(Seek Pagination):适合无限滚动场景
用上一页最后一条记录的排序字段值做边界条件,每次都是范围扫描,性能恒定。前端是“下拉加载更多”时首选。
第一页:SELECT * FROM t_order WHERE status = 'PAID' ORDER BY create_time DESC, id DESC LIMIT 10;
第二页(假设上一页最后一条是 create_time = '2023-10-01 12:00:00', id = 12345):SELECT * FROM t_order WHERE status = 'PAID' AND (create_time
- 必须保证
ORDER BY字段组合能唯一确定一行(推荐加id作为第二排序字段) - WHERE 条件要严格对应排序方向(
DESC就用或 <code>AND组合) - 不能跳页,只能顺序下翻;用户手动输入页码时需 fallback 到延迟关联或禁用
- 时间字段精度不足(如只到秒)时,
create_time相同的多条记录容易漏掉
真正难的不是写对 SQL,而是判断该用哪种分页逻辑——游标分页要改前后端交互方式,延迟关联要补索引和重构 SQL,而继续硬扛 LIMIT OFFSET 只会让问题在某天凌晨三点突然爆炸。别等 EXPLAIN 显示 rows 过百万才想起这事。











