详解Mysql解决深分页方案
MySQL 解决深分页(如 LIMIT 1000000, 10
)的核心思路是 避免扫描和丢弃大量无用数据。
一、问题本质:为什么深分页慢?
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10;
执行过程:
扫描前 1,000,010 行
丢弃前 1,000,000 行
返回最后 10 行
代价:O(N) 复杂度,百万级数据需 1-10 秒
二、七大解决方案
方案 1:游标分页(最优解)
原理:记录上一页最后一条记录的 ID,直接定位起点
-- 第一页(常规)
SELECT * FROM orders ORDER BY id DESC LIMIT 10;-- 下一页(核心!)
SELECT * FROM orders
WHERE id < 上一页最后ID -- 直接跳过已读数据
ORDER BY id DESC
LIMIT 10;
优点:
时间复杂度 O(10) → 恒定为 10 行扫描
支持顺序/倒序翻页
数据一致性高(无新增删除时)
限制:
只能顺序翻页,不能跳页
需前端传递最后一条记录的 ID
方案 2:延迟关联(覆盖索引优化)
原理:先通过索引定位 ID,再回表取数据
SELECT * FROM orders
INNER JOIN (SELECT id FROM orders -- 只查索引ORDER BY create_timeLIMIT 1000000, 10
) AS tmp USING(id); -- 回表取完整数据
优势:
子查询仅扫描索引(体积小)
减少 90% 以上的回表量
尤其适合
ORDER BY
非主键列的场景
性能对比:
原查询 | 延迟关联 |
---|---|
扫描 1,000,010 行数据 | 扫描 1,000,010 行索引 |
1,000,010 次回表 | 仅 10 次回表 |
方案 3:范围分页(ID 分段)
原理:用 WHERE
替代 OFFSET
SELECT * FROM orders
WHERE id BETWEEN 1000000 AND 1000010; -- 精确范围
适用场景:
主键连续且无删除
已知分页边界值(如每页固定 1000 条)
方案 4:业务降级(近似分页)
策略:
限制深度:
WHERE page_no <= 100
(禁止查 100 页后)模糊跳页:只提供「上一页/下一页」,不显示总页数
滚动加载:用 JS 实现无限滚动,避免传统分页
方案 5:预计算分页(空间换时间)
步骤:
创建分页映射表:
CREATE TABLE page_index (page INT PRIMARY KEY,min_id INT,max_id INT );
定期更新分页范围
查询时直接定位:
SELECT * FROM orders WHERE id BETWEEN (SELECT min_id FROM page_index WHERE page=100)AND (SELECT max_id FROM page_index WHERE page=100);
方案 6:专用搜索引擎
工具选择:
Elasticsearch:
search_after
API 支持深度游标ClickHouse:内置分页优化引擎
TiDB:分布式数据库线性扩展
方案 7:分区表(MySQL 8.0+)
利用分区裁剪:
-- 按范围分区
CREATE TABLE orders (id INT AUTO_INCREMENT,...PRIMARY KEY(id)
) PARTITION BY RANGE(id) (PARTITION p0 VALUES LESS THAN (100000),PARTITION p1 VALUES LESS THAN (200000),...
);-- 查询时自动过滤无关分区
SELECT * FROM orders
WHERE id > 1000000
ORDER BY id LIMIT 10;
三、方案选择决策树
四、性能对比测试(1000万行数据)
方案 | 页数 | 耗时 | 扫描行数 |
---|---|---|---|
原始 LIMIT | 第50万页 | 4.2s | 5,000,010 |
延迟关联 | 第50万页 | 0.8s | 5,000,010* |
游标分页 | 第50万页 | 0.003s | 10 |
Elasticsearch | 第50万页 | 0.15s | 10 |
*:延迟关联仅索引扫描,成本远低于数据扫描
五、终极建议
首选游标分页:99% 的场景适用,需前后端配合
次选延迟关联:兼容传统分页 API
禁用 OFFSET:在代码规范中明确禁止
offset > 10000
监控慢查询:捕获深分页 SQL
-- 查找可疑深分页 SELECT * FROM sys.slow_log WHERE sql_text LIKE '%LIMIT%,%';