优化千万级表分页的核心是设计高效索引:ORDER BY字段必须有索引避免filesort;联合索引按“过滤列+排序列”顺序排列且方向一致;用覆盖索引减少回表;游标分页比LIMIT更稳定;务必通过EXPLAIN验证索引实际使用效果。

直接用 LIMIT offset, size 查千万级表,哪怕加了索引也慢——根本原因不是没建索引,而是索引没被高效复用。优化分页的索引设计,核心是让 MySQL 能跳过大量数据、避免回表、不触发 filesort。
为什么ORDER BY字段必须有索引
没有索引的 ORDER BY 会强制 MySQL 做 filesort(内存或磁盘排序),数据量一上百万,性能断崖式下跌。哪怕 WHERE 条件走了索引,只要排序字段没索引,前面的索引也白搭。
- 错误写法:
SELECT * FROM orders WHERE status = 1 ORDER BY create_time LIMIT 20—— 如果create_time没索引,就全表扫描 + 排序 - 正确做法:建联合索引
idx_status_ctime,顺序为(status, create_time) - 注意:如果查询还带
WHERE status IN (1,2)或status > 0,create_time就无法走索引排序,得换status = ? AND create_time > ?这类等值+范围组合
联合索引怎么排字段顺序才对分页友好
分页场景下,索引字段顺序直接影响能否跳过 offset。必须把「过滤条件列」放前面,「排序列」紧随其后,且排序列要是确定方向的(ASC/DESC 要和索引定义一致)。
- 例如分页语句:
SELECT * FROM orders WHERE user_id = 123 ORDER BY create_time DESC LIMIT 20 - 应建索引:
ALTER TABLE orders ADD INDEX idx_user_ctime (user_id, create_time DESC) - 不能反过来建
(create_time, user_id):WHERE 条件跳过了最左列,整个索引失效 - MySQL 8.0+ 支持显式
DESC索引;8.0 之前建(user_id, create_time),但查询必须用ORDER BY create_time ASC才能走索引
覆盖索引怎么减少回表开销
分页时如果只查 ID 和少量字段,用覆盖索引能省掉 90% 以上的 IO。一旦要查 *,就得回表——而深度分页时回表是随机 IO,比顺序扫描还慢。
- 先用子查询只取主键:
SELECT id FROM orders WHERE status = 1 ORDER BY create_time DESC LIMIT 100000, 20 - 这个子查询必须命中覆盖索引,比如
idx_status_ctime_id定义为(status, create_time DESC, id) - 外层再 JOIN:
SELECT o.* FROM orders o JOIN (...) t ON o.id = t.id - 别偷懒建
(status, create_time, *)—— 索引不能包含所有字段,只把 SELECT 列里实际用到的字段加进去
主键自增表还能不能用id > last_id分页
可以,但前提是业务允许「只能下一页」「不能跳页」,且排序逻辑和主键顺序一致。否则会出现漏数据或重复。
- 安全用法:
SELECT * FROM orders WHERE id > 1000000 ORDER BY id ASC LIMIT 20(id 自增,且按 id 分页) - 危险用法:
SELECT * FROM orders WHERE id > 1000000 ORDER BY create_time DESC LIMIT 20—— id 和时间不严格正相关,会漏掉中间插入的旧时间新记录 - 真正健壮的游标分页要带两个字段:
WHERE create_time <= ? AND (create_time < ? OR id < ?),对应上一页最后一条的(create_time, id) - 这个游标组合必须有联合索引支撑,比如
idx_ctime_id (create_time DESC, id DESC)
最容易被忽略的一点:索引不是建完就生效。EXPLAIN 的 Extra 列出现 Using filesort 或 Using temporary 就说明索引没切中要害;出现 Using index 才算真正用上了覆盖索引。别信“我建了索引”,要看执行计划里它到底干了什么。


















