背景与问题
在业务系统中,分页查询是最常见的数据库操作之一。然而,当数据量达到百万级,使用传统的 LIMIT offset, size 进行深分页(如 LIMIT 1000000, 20)时,性能会急剧下降。原因在于 MySQL 需要扫描并丢弃前 offset 行,才能返回目标数据,这种全表扫描的代价随 offset 增大而线性增长。
本文将以一个实际订单表(500万行数据)为例,对比三种主流优化方案:游标分页 、延迟关联 、子查询优化,并给出实测数据与适用场景。
方案一:游标分页(基于排序键)
核心思想:不指定 offset,而是记住上一页最后一条记录的排序键值,查询时用 WHERE id > last_id 定位。
-- 第一页
SELECT * FROM orders ORDER BY id LIMIT 20;
-- 下一页,传入上一页最后一条记录的 id
SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 20;
优点:性能恒定,与页码无关;索引利用率高;代码简单。
缺点:只能按单一排序字段(通常是主键)进行;无法实现跳页(如直接跳到第100页);如果排序字段有重复值,需要额外处理。
适用场景:无限滚动、移动端加载更多、API 分页。
方案二:延迟关联(覆盖索引 + 回表)
核心思想:先通过覆盖索引快速定位所需行的主键,再与原表关联获取完整数据。
SELECT o.* FROM orders o
INNER JOIN (
SELECT id FROM orders ORDER BY id LIMIT 1000000, 20
) AS tmp ON o.id = tmp.id;
优点:避免了全行扫描,只扫描索引;相比传统 LIMIT 性能提升明显。
缺点:仍然需要扫描 offset 行(但只扫索引),offset 很大时性能依然下降;需要额外 join 操作。
适用场景:需要跳页,且排序字段有索引(不一定是主键)时。
方案三:子查询优化(基于索引覆盖)
核心思想:先通过子查询快速定位起始位置,再取后续数据。
SELECT * FROM orders
WHERE id >= (SELECT id FROM orders ORDER BY id LIMIT 1000000, 1)
ORDER BY id
LIMIT 20;
优点:无需 join,单次查询;相比延迟关联更简洁。
缺点:子查询仍需要扫描 offset 行;依赖主键或唯一索引。
适用场景:适合单表查询,且排序字段为主键时。
实测对比(500万行数据)
测试环境:MySQL 8.0,InnoDB,订单表 500 万行,主键 id。查询 LIMIT 1000000, 20。
| 方案 | 执行时间(ms) | 扫描行数 |
|---|---|---|
| 传统 LIMIT | 482 | 1000020 |
| 延迟关联 | 126 | 1000020(索引) |
| 子查询优化 | 98 | 1000020(索引) |
| 游标分页 | 2 | 20 |
可见,游标分页性能最优,但牺牲了跳页能力;延迟关联和子查询优化能缓解深分页问题,但 offset 很大时依然有性能瓶颈。
总结与选型建议
- 优先使用游标分页:如果你的业务允许(如无限滚动、加载更多),这是性能最好的方案。
- 需要跳页时:使用延迟关联或子查询优化,并确保排序字段有索引。
- 避免过深 offset:如果业务必须跳页,可以考虑限制最大页码,或使用缓存。
最后,无论哪种方案,都应配合索引优化和查询分析(EXPLAIN)进行验证,切勿盲目套用。