LIMIT offset, size 越大越慢,因需生成全量JOIN结果再丢弃前offset行;应改用子查询ID分页或游标分页,并确保排序字段有唯一性补全和对应索引。

直接在多表 JOIN 结果上写 LIMIT offset, size 会越来越慢,不是因为语法错,而是数据库必须先生成完整中间结果集(可能膨胀几倍),再扫描并丢弃前 offset 行——offset 越大,丢弃越多,性能断崖下跌。
为什么 LIMIT 10000, 20 比 LIMIT 0, 20 慢几十倍
常见现象:EXPLAIN 显示 rows 暴增、出现 Using temporary; Using filesort。根本原因是:
- 数据库先执行全部
JOIN、WHERE、ORDER BY,中间结果集可能达数十万行(比如主表 1 万行 × 关联明细平均 5 行) -
ORDER BY字段若没索引、或含函数(如RAND())、或排序字段重复无唯一补全,就无法用索引快速定位起始位置 - 即使
JOIN字段有索引,一旦驱动表选错(比如用千万级表做左表LEFT JOIN),也会导致嵌套循环外层循环次数爆炸
用子查询先取 ID 再 JOIN(适合需跳页的场景)
核心是把分页逻辑下推到主表索引扫描阶段,避免全量关联。必须同时满足三个条件才真正有效:
- 子查询只查主表的
id(或主键),且ORDER BY字段有索引(如created_at) - 子查询里不能出现任何从表字段、
JOIN、GROUP BY或函数表达式 - 外层
JOIN必须用该id关联,不能改用其他字段(否则优化器无法走主键索引)
正确示例:
SELECT o.*, u.name, p.title FROM orders o INNER JOIN ( SELECT id FROM orders WHERE status = 1 ORDER BY created_at DESC, id DESC LIMIT 20 OFFSET 40000 ) AS page_ids ON o.id = page_ids.id LEFT JOIN users u ON o.user_id = u.id LEFT JOIN products p ON o.product_id = p.id;
错误写法:SELECT o.id FROM orders o JOIN users u ... —— 这会让优化器放弃索引下推,回到全量 JOIN 后分页的老路。
改用游标分页替代 OFFSET(适合无限滚动)
当偏移量持续增长(如用户不断点“下一页”),游标分页比任何 LIMIT offset 都可靠。前提是:
- 游标字段必须来自驱动表(即
FROM后的第一张表),且有索引、非空、唯一(推荐主键或带唯一约束的业务字段) - 复合游标(如
(created_at, id))要求联合索引顺序与WHERE条件完全一致,且所有字段非NULL - 前端必须保存上一页末尾的游标值,不能跳页;例如上一页最后一条是
created_at = '2025-03-01', id = 88888,下一页查:WHERE (created_at, id) > ('2025-03-01', 88888) ORDER BY created_at, id LIMIT 20
错误示例:WHERE o.created_at > ? 但 orders 不是驱动表 —— 条件无法下推,仍要先 JOIN 再过滤。
最易被忽略的一点:排序字段重复时,OFFSET 分页结果不稳定。哪怕加了索引,仅按 created_at DESC 排序,多个订单时间相同时,每次执行可能取到不同行。必须强制补全唯一字段,如 ORDER BY created_at DESC, id DESC,且该组合要有联合索引。

















