在 MySQL 中,分页方案的性能优劣取决于数据量、索引情况、排序条件,没有绝对 “最好” 的方案,只有 “最适合当前场景” 的选择。

以下是主流分页方案的详细对比、适用场景及优化建议,帮你快速选型:
一、核心分页场景
先明确分页的核心需求:从大量数据中按条件(如排序、筛选)截取部分结果(如第 N 页,每页 M 条),核心痛点是大数据量下 “跳过前 N 条” 导致的性能损耗。
二、主流分页方案对比
方案 1:Limit 偏移量分页(最常用,但大数据量低效)
语法
sql
-- 第 page 页,每页 size 条(page 从 1 开始)
SELECT * FROM table
WHERE condition
ORDER BY id DESC
LIMIT (page-1)*size, size;
原理
通过 OFFSET 跳过前 (page-1)*size 条数据,再取 size 条。(注:LIMIT offset, size 等价于 LIMIT size OFFSET offset)
优点
- 语法简单,开发成本低;
- 适合小数据量、前几页(如 page ≤ 100)的场景。
缺点
- 大数据量 + 深分页(如 page=1000,size=20)时,MySQL 需扫描前
1000*20=20000条数据,再丢弃前 19980 条,IO 开销极大; - 无索引或排序字段无索引时,会触发全表扫描,性能暴跌。
优化前提
- 排序字段(如
id)必须建立索引(最好是主键或唯一索引,有序且查询快); - 避免
SELECT *,只查询需要的字段(减少数据传输和内存占用)。
方案 2:基于游标(ID)的分页(大数据量深分页最优)
语法
sql
-- 上一页最后一条数据的 ID 为 last_id(首次查询 last_id=0)
SELECT * FROM table
WHERE condition AND id > last_id -- 利用索引快速定位
ORDER BY id ASC
LIMIT size;
原理
不使用 OFFSET 跳过数据,而是通过上一页的最后一条记录的索引字段(如主键 ID) 作为条件,直接定位到分页起点,再取 size 条。核心是利用索引的有序性,避免扫描无用数据。
优点
- 性能极致:无论多少页,都只需扫描
size条数据(索引查找 + 范围扫描),时间复杂度 O (1); - 无深分页问题:适合百万 / 千万级数据的深分页。
缺点
- 不支持 “跳页”(如直接跳转到第 100 页),只能 “上一页 / 下一页”;
- 排序字段必须是唯一、有序的(如主键 ID、自增字段),且需建立索引;
- 若排序条件是多字段(如
ORDER BY create_time DESC, id DESC),需建立联合索引(create_time, id),且游标需携带两个字段的值(如last_create_time和last_id)。
扩展:多字段排序的游标分页
sql
-- 联合索引:(create_time DESC, id DESC)
SELECT * FROM table
WHERE create_time < last_create_time
OR (create_time = last_create_time AND id < last_id)
ORDER BY create_time DESC, id DESC
LIMIT size;
方案 3:子查询定位起点(优化 Limit 深分页)
语法
sql
-- 先通过子查询找到第 (page-1)*size+1 条数据的 ID,再关联查询
SELECT t.* FROM table t
JOIN (
SELECT id FROM table
WHERE condition
ORDER BY id DESC
LIMIT (page-1)*size, 1 -- 只查分页起点的 ID,索引扫描快
) AS tmp ON t.id = tmp.id
ORDER BY t.id DESC
LIMIT size;
原理
利用 “索引覆盖查询”(子查询只查 id,无需回表)快速找到分页起点的 id,再通过 id 关联查询完整数据,避免扫描大量无用数据。
优点
- 兼容 “跳页” 需求,比原生
Limit offset快 10~100 倍(大数据量下); - 排序字段只需建立普通索引,无需唯一约束。
缺点
- 语法比原生
Limit复杂,需确保子查询的ORDER BY与主查询一致; - 若条件过滤后的数据量极小,性能优势不明显。
方案 4:使用物化视图 / 预计算(超大数据量静态分页)
适用场景
数据更新频率极低(如报表、历史数据),且需要频繁深分页查询。
原理
- 提前按排序条件将数据分块存储(如每 100 条为一块,记录块的起始 ID 和结束 ID);
- 分页时直接通过块信息定位到目标块,再查询块内数据。
实现示例
- 创建预计算表:
sql
CREATE TABLE page_block (
block_id INT PRIMARY KEY AUTO_INCREMENT,
start_id INT NOT NULL, -- 块内第一条数据的 ID
end_id INT NOT NULL, -- 块内最后一条数据的 ID
total INT NOT NULL -- 块内数据条数
);
- 定时任务更新预计算表(如每天凌晨执行):
sql
-- 按 id 排序,每 100 条分一块
INSERT INTO page_block (start_id, end_id, total)
SELECT
MIN(id) AS start_id,
MAX(id) AS end_id,
COUNT(*) AS total
FROM (
SELECT id, @row := @row + 1 AS row_num
FROM table, (SELECT @row := 0) AS tmp
ORDER BY id DESC
) AS t
GROUP BY row_num DIV 100; -- 每 100 条分一块
- 分页查询时,先查块信息,再查数据:
sql
-- 查第 10 页(size=100),对应 block_id=10
SELECT t.* FROM table t
JOIN page_block b ON t.id BETWEEN b.start_id AND b.end_id
WHERE b.block_id = 10
ORDER BY t.id DESC;
优点
- 超大数据量下分页速度极快(直接定位块,无需扫描);
- 支持跳页和任意页查询。
缺点
- 数据实时性差(依赖定时更新);
- 增加存储成本和维护成本。
三、性能对比
| 方案 | 适用场景 | 优点 | 缺点 | 大数据量深分页性能 |
|---|---|---|---|---|
| Limit 偏移量 | 小数据量、前几页、需跳页 | 语法简单、支持跳页 | 深分页扫描量大、性能差 | ❌ 差 |
| 游标(ID)分页 | 大数据量、深分页、只需要上下页 | 性能极致、无扫描损耗 | 不支持跳页、依赖有序唯一索引 | ✅ 最优 |
| 子查询定位起点 | 大数据量、深分页、需跳页 | 兼容跳页、性能优于原生 Limit | 语法复杂、需一致排序 | ✅ 优秀 |
| 物化视图 / 预计算 | 超大数据量、静态数据、频繁深分页 | 速度极快、支持任意页 | 实时性差、维护成本高 | ✅ 超优(静态场景) |
四、选型建议
- 优先选游标分页:如果业务支持 “上一页 / 下一页”(如列表滚动加载),无论数据量大小,游标分页都是性能最优解;
- 需跳页选子查询定位:如果必须支持跳页(如分页控件选择第 N 页),且数据量较大(百万级),用子查询定位起点替代原生
Limit; - 小数据量直接用 Limit:数据量 ≤ 10 万,且分页不超过前 100 页,原生
Limit足够,开发效率优先; - 静态数据用预计算:数据更新极少,且需要频繁深分页(如千万级历史数据报表),用物化视图 / 预计算表。
五、通用优化技巧
- 索引优先:排序字段(如
id、create_time)必须建立索引,多字段排序建立联合索引(顺序与ORDER BY一致); - 避免全表扫描:
WHERE条件中的筛选字段尽量建立索引,减少扫描的数据量; - 不查无用字段:用
SELECT col1, col2替代SELECT *,减少数据传输和内存占用; - 分页大小限制:限制最大分页页数(如最多支持前 1000 页),避免恶意深分页查询拖垮数据库;
- 使用覆盖索引:如果分页只需要部分字段(如列表页只显示标题和时间),可建立覆盖索引(包含查询字段),避免回表查询,提升性能。
示例:覆盖索引优化
sql
-- 需求:分页查询文章的 id、title、create_time(按 create_time 排序)
-- 建立覆盖索引:包含查询字段和排序字段
CREATE INDEX idx_article_cover ON article (create_time DESC, id, title);
-- 查询时无需回表,直接从索引获取数据
SELECT id, title, create_time FROM article
ORDER BY create_time DESC
LIMIT 1000, 20;
工具:可通过 Sharding-JDBC 等中间件自动分库分表,降低开发成本。
六、避坑指南(常见错误优化)
- 不要给
OFFSET加索引:OFFSET是逻辑偏移,MySQL 无法直接通过索引优化,优化的核心是 “排序字段 + 过滤条件” 的索引; - 避免
ORDER BY RAND()与LIMIT混用:RAND()会导致全表扫描并生成临时表,性能极差,若需随机分页,可通过 “随机主键” 替代; - 不要过度依赖
LIMIT深分页:即使优化后,LIMIT 1000000, 20仍不如游标分页高效,能不用深分页就不用。
七、优化效果对比(以 1000 万条数据为例)
| 优化方案 | SQL 示例 | 执行时间(参考) | 核心提升点 |
|---|---|---|---|
| 原生 LIMIT(无索引) | SELECT * FROM article LIMIT 100000,20 | 5~10 秒 | 无 |
| 基础优化(加索引 + 少字段) | SELECT id,title FROM article ORDER BY id LIMIT 100000,20 | 500~800 毫秒 | 索引避免全表扫描,少字段减少传输 |
| 子查询定位起点 | JOIN (SELECT id LIMIT 100000,1) ON t.id=tmp.id | 50~100 毫秒 | 避免扫描前 100000 条数据 |
| 主键过滤(游标思想) | SELECT id,title FROM article WHERE id<100000 LIMIT 20 | 1~5 毫秒 | 索引直接定位起点,无扫描 |
| 预计算分页块 | JOIN page_block WHERE block_id=501 | 1~3 毫秒 | 直接定位块,无需计算偏移量 |
总结
- 小数据量(≤10 万)+ 前几页:基础优化(加索引 + 少字段)即可;
- 大数据量(百万级)+ 深分页 + 跳页:子查询定位起点 / 主键过滤;
- 大数据量(千万级 +)+ 静态数据:预计算分页块;
- 大数据量(亿级)+ 动态数据:分库分表;
- 能不用深分页就不用:优先设计 “上一页 / 下一页” 的游标分页,性能最优。
核心原则:让 MySQL 尽可能少地扫描数据,利用索引快速定位目标范围。






评论
抢沙发