limit offset, size在大数据偏移量下变慢,是因为mysql需先排序完整结果集再逐行扫描丢弃前offset行;推荐用where+上一页末尾值替代offset,如select * from t where id > 6800000 order by id limit 20。

直接用 LIMIT offset, size 查千万级表的后几百页,基本等于让 MySQL 扫描并丢弃上百万行——不优化就上线,接口超时是常态。
为什么子查询能绕过 LIMIT 的扫描陷阱
MySQL 执行 LIMIT 1000000, 20 时,并不会跳到第 1000001 行再读,而是从头扫出 1000020 行,再扔掉前 1000000 行。子查询的关键在于:先只查主键(或索引覆盖字段),利用索引快速定位目标 ID 区间,再回表取完整数据。
这背后依赖两个事实:ORDER BY 字段必须有索引;子查询里只 SELECT id,避免回表和大字段加载。
- 如果
ORDER BY字段没索引,子查询本身就会触发Using filesort,优化失效 - 子查询中不能带
WHERE过滤(除非该条件能走索引且不破坏排序连续性),否则 ID 序列不连续,LIMIT偏移会错位 - 复合索引顺序很重要:比如按
updated_at DESC, id ASC排序,索引应建为(updated_at, id),否则无法覆盖排序+定位
延迟关联写法怎么写才真正生效
延迟关联本质是把“全字段扫描 + 跳行”拆成两步:第一步轻量索引扫描拿到 ID 列表,第二步用这些 ID 精确回表。它比纯子查询更可控,尤其适合 SELECT * 场景。
典型写法:
SELECT t.* FROM `order` AS t INNER JOIN ( SELECT id FROM `order` WHERE status = 1 ORDER BY created_at DESC LIMIT 1000000, 20 ) AS tmp ON t.id = tmp.id;
注意点:
- 外层
t.*不会放大扫描量,因为tmp子查询已限定只取 20 个id -
WHERE条件必须落在索引最左前缀上,否则子查询仍会全表扫描 - 如果表用的是
datetime类型时间字段排序,且存在大量重复值,建议在索引中加入主键作为第二列,避免排序不确定性
容易被忽略的索引细节和执行计划验证
很多人加了索引却没效果,问题常出在执行计划没看懂。用 EXPLAIN 检查时,重点盯三个字段:type(应为 range 或 index)、key(是否命中预期索引)、rows(预估扫描行数是否接近 LIMIT 值)。
常见坑:
- 写了
ORDER BY created_at DESC,但索引是(created_at)升序 —— MySQL 5.7 不支持降序索引,得靠反向扫描,性能打折;MySQL 8.0+ 可建(created_at DESC)显式降序索引 - 联合索引中把过滤字段放后面,比如
(id, created_at),而查询条件是WHERE created_at > '2025-01-01'—— 索引无法使用 - 子查询用了
LIMIT,但外层 JOIN 没加STRAIGHT_JOIN,优化器可能调换驱动表顺序,导致全表扫描
游标分页不是万能解,但比 OFFSET 更适合同步类场景
像定时任务拉取增量数据这种场景,用户不跳页、只顺序往后翻,用游标(即记录上一批最后一条的 id 或 updated_at)是最省资源的。例如:
SELECT * FROM `MCS_PROD` WHERE UPDT_TIME >= '2026-05-01 00:00:00' AND id > 6845221 ORDER BY UPDT_TIME, id LIMIT 1000;
这个查询几乎不依赖偏移量,B+ 树只需从指定位置往后顺序读。但它要求业务接受“不能跳页”和“必须维护上次结束点”的约束。一旦中间某次失败,需要重置游标,否则漏数据。
真正难处理的是管理后台那种支持任意页码输入、还要显示总页数的场景——这时候要么缓存总条数(用 COUNT(*) 加覆盖索引),要么前端放弃总页数显示,改用“下一页/上一页+加载更多”模式。











