LIMIT 100000, 20会扫描至少100020行,因MySQL必须逐行读取、排序并丢弃前100000行,才能返回后20行,无法跳过偏移量,导致I/O和CPU大量浪费。

为什么LIMIT 100000, 20会扫描100020行
MySQL执行 LIMIT offset, size 时,不管有没有索引,都得先定位到第 offset + size 行——它不会跳过前 offset 行,而是老老实实扫描、排序、再丢弃。比如 LIMIT 100000, 20,优化器仍要读取至少 100020 行数据(可能更多,尤其有WHERE过滤时),再扔掉前10万条。EXPLAIN里看到的 rows 值飙升、Extra 出现 Using filesort 或 Using index condition,基本就是这个症状。
覆盖索引分页:SELECT id FROM ... LIMIT ... 再 IN 关联
核心是让第一层查询「只走索引、不回表」,把耗时大户(I/O、临时排序)压到最低。
- 建联合索引必须包含
WHERE条件列 +ORDER BY列 + 主键(如id),例如:ALTER TABLE orders ADD INDEX idx_user_status_ctime (user_id, status, create_time, id) - 分页查询只查索引列:
SELECT id FROM orders WHERE user_id = 100 AND status = 1 ORDER BY create_time DESC LIMIT 100000, 20;此时EXPLAIN的Extra应为Using index - 拿到
id列表后,用IN查详情:SELECT * FROM orders WHERE id IN (12345, 12346, ...);注意控制IN长度,别超max_allowed_packet(一般 ≤ 1000 个ID) - 避免直接
SELECT *套子查询:SELECT * FROM orders WHERE id IN (SELECT id FROM orders ... LIMIT ...)不合法——MySQL 不允许子查询中用LIMIT
游标分页替代 offset:WHERE id > ? 是更稳的选择
适用于主键或时间字段单调递增、无重复、业务允许顺序翻页的场景(如订单流、日志列表)。它彻底绕开 offset 扫描,从锚点直接范围读取。
- 第一页查出最后一条的
id和create_time(复合排序需两者都记):SELECT * FROM orders WHERE user_id = 100 ORDER BY create_time DESC, id DESC LIMIT 20 - 下一页用上一页最后一条的值做条件:
SELECT * FROM orders WHERE user_id = 100 AND (create_time - 索引必须匹配排序顺序,否则触发
filesort;若create_time允许 NULL,记得在 WHERE 中显式排除或在索引中定义NOT NULL - 不能跳页,也不适合用户手动输入页码——这是 trade-off,不是 bug
容易被忽略的覆盖索引失效点
加了索引还慢?大概率栽在这几个细节上:
-
ORDER BY字段含大量NULL,且索引未声明NOT NULL,MySQL 可能放弃用该索引排序 - 联合索引顺序和查询逻辑不一致,比如
INDEX (id, create_time)却写ORDER BY create_time DESC,无法利用索引有序性 -
WHERE条件用了函数或隐式转换,例如WHERE DATE(create_time) = '2026-05-01'或WHERE user_id = '100'(字段是INT),索引直接失效 - 查询列超出索引覆盖范围,比如索引是
(user_id, create_time),但写了SELECT user_id, create_time, amount,amount不在索引里,就得回表
真正卡住的从来不是“要不要加索引”,而是索引建在哪、怎么用、以及查询语句有没有悄悄把它废掉。游标和覆盖索引不是二选一,而是根据接口是否支持跳页、字段是否唯一递增、业务能否接受状态耦合来决定用哪条路。


















