当表里的数据量达到几万甚至几百万行时,前端页面不可能把所有数据一次性展示出来,通常的做法是每页显示10条或20条,用户点击下一页时再查询下一段数据,这就是分页查询。MySQL提供了LIMIT和OFFSET关键字来完成这件事,用法看似简单,但不同的写法在性能和数据准确性上差别很大。这篇文章从基本语法讲起,逐步深入到执行原理和大分页优化。

LIMIT和OFFSET的基本语法与常见写法
MySQL中最常见的分页写法有两种。LIMIT 10 OFFSET 20表示跳过前20行,然后返回10行数据;等价的简写形式是LIMIT 20, 10,逗号前的数字是偏移量,逗号后的数字是返回的行数。两种写法效果完全一样,日常开发中简写形式更常见。
例如查询用户表第3页的数据,每页显示20条,第一页的偏移量是0,第3页的偏移量就是 (3-1) × 20 = 40,对应的SQL如下:
-- 写法一:LIMIT 行数 OFFSET 偏移量 SELECT id, username, email FROM users ORDER BY id LIMIT 20 OFFSET 40; -- 写法二:LIMIT 偏移量, 行数(逗号写法) SELECT id, username, email FROM users ORDER BY id LIMIT 40, 20;
有几点细节需要注意。LIMIT 0, 20表示从第一行开始取20条,偏移量是从0开始计数的,这一点和数组下标类似,初学者容易把第一页写成OFFSET 1,导致第一页永远少一条数据。另外,如果偏移量超出了总行数,比如表里只有100行却写了LIMIT 20 OFFSET 200,MySQL不会报错,只是返回空结果集。还有一种特殊情况,只写LIMIT 10表示从头取10条,等价于LIMIT 10 OFFSET 0,常用来查询前N条记录。
ORDER BY与分页:必须先解决排序问题
分页的前提是数据有稳定的顺序,否则每一页的内容就是随机的,翻页时可能出现同一条记录在不同页重复出现的问题。这就要求分页SQL必须配合ORDER BY使用,而且排序字段最好是唯一字段或者组合起来唯一的字段,比如主键id。
如果排序字段不唯一,比如按created_time排序,而很多记录的时间相同,MySQL在不同查询中的排序结果可能不稳定。第一页取到的某条数据,在查询第二页时可能因为排序顺序变化又出现了,或者某条数据被跳过去了。解决办法是加上一个唯一字段做第二排序条件:
-- 不稳定的写法:created_time 可能重复 SELECT id, username FROM users ORDER BY created_time LIMIT 20 OFFSET 40; -- 稳定的写法:加上 id 保证排序唯一 SELECT id, username FROM users ORDER BY created_time, id LIMIT 20 OFFSET 40;
另外要注意排序字段是否有索引。如果ORDER BY的字段没有索引,MySQL会先扫描所有符合条件的行,进行文件排序(Using filesort),数据量大时CPU和内存开销都不小。用EXPLAIN分析执行计划,如果Extra列出现了Using filesort,就应该考虑为排序字段建立索引,让MySQL直接利用索引的有序性,避免额外的排序步骤。
深分页性能问题:为什么OFFSET越大越慢
LIMIT OFFSET最大的问题在于OFFSET的工作方式。执行LIMIT 10 OFFSET 1000000时,MySQL并不是直接定位到第1000001行,而是从头开始扫描,先取出1000010行,再把前1000000行丢弃,只返回最后10行。被跳过的行虽然不返回给客户端,但扫描和读取的开销一点没少,offset越大,浪费的工作越多。
在一个千万级数据的表上做测试,OFFSET为10时查询耗时几毫秒,OFFSET为500万时可能需要好几秒,性能差距上百倍。这就是很多系统翻到几百页之后越来越卡的根本原因。可以用EXPLAIN观察:即使只返回10行,rows列显示的扫描行数仍然是offset + limit的总和。
针对深分页,业内有几种成熟的优化方案。第一种是覆盖索引加延迟关联:先在索引上完成分页定位拿到主键,再回表取完整字段,大幅减少回表次数:
-- 直接分页:需要回表 offset+limit 次
SELECT id, username, email, created_time
FROM users
ORDER BY id
LIMIT 10 OFFSET 1000000;
-- 延迟关联:子查询只扫描索引,回表仅10次
SELECT u.id, u.username, u.email, u.created_time
FROM users u
INNER JOIN (
SELECT id FROM users
ORDER BY id
LIMIT 10 OFFSET 1000000
) t ON u.id = t.id;
第二种方案是书签方式,也叫游标分页或keyset分页。它的思路是记住上一页最后一条记录的排序值,下一页查询时用WHERE条件直接定位,完全不使用OFFSET:
-- 记住上一页最后的 id 是 1000000,下一页直接从它之后取 SELECT id, username, email FROM users WHERE id > 1000000 ORDER BY id LIMIT 10;
这种写法无论翻到第几页,扫描的行数始终接近limit值,性能稳定。它的限制是只能支持上一页下一页的连续翻页,无法直接跳转到任意页。如果产品上确实需要跳页,可以限制最大页数,或者结合延迟关联方案,把OFFSET控制在合理范围内。绝大多数互联网产品(比如信息流、订单列表)都采用游标分页,正是因为它在大数据量下表现稳定。
分页总数的计算与开发实践建议
分页页面通常还需要展示总记录数和总页数,常见的做法是执行SELECT COUNT(*) FROM users WHERE ...。在InnoDB引擎下COUNT需要扫描索引,数据量大时也比较慢。如果业务不要求精确总数,可以改为查询固定阈值,比如LIMIT 200001只取到20万就停,超过就显示20万+,很多电商和社交产品都是这么处理的。
综合来看,写分页SQL时建议遵循几个原则:分页必须配合ORDER BY且保证排序唯一;排序字段尽量走索引;小数据量直接用LIMIT OFFSET即可,不必过度优化;数据量大或翻页深时优先考虑游标分页,其次选择延迟关联;最后别忘了在上线前用EXPLAIN检查执行计划,确认扫描行数在预期范围内。掌握这些要点,就能应对MySQL分页的绝大多数场景。
MySQL分页LIMIT OFFSET分页查询优化修改时间:2026-09-04 18:54:38