MySQL分页首选LIMIT+OFFSET,需配合ORDER BY及索引;OFFSET过大时性能骤降,应改用基于唯一有序字段的游标分页(如WHERE id > ? LIMIT ?)。

MySQL 分页用 LIMIT 和 OFFSET 最直接
绝大多数分页场景,靠 LIMIT + OFFSET 就能解决。它语法简单,语义清晰:跳过前 OFFSET 行,取接下来 LIMIT 行。
比如查第 3 页、每页 10 条:
SELECT * FROM orders ORDER BY created_at DESC LIMIT 10 OFFSET 20;
注意:这里的 OFFSET 20 表示跳过前 20 行(即前 2 页),不是“从第 20 行开始”。容易误写成 OFFSET 30 或漏掉 ORDER BY —— 没有明确排序时,分页结果不可预测,同一页可能反复出现/丢失数据。
-
OFFSET值 =(page_no - 1) * page_size,别手算错 - 必须配合
ORDER BY,且排序字段最好有索引(如created_at或主键) - 当
OFFSET很大(比如 > 10 万),性能会明显下降——MySQL 仍需扫描并丢弃前面所有行
大数据量下用游标分页(WHERE id > ? LIMIT ?)更高效
当总记录数超百万、用户频繁翻到几十页以后,OFFSET 方式变慢甚至超时。此时应改用基于上一页最后 ID 的游标分页(也叫 keyset 分页)。
假设你刚查完第 2 页,最后一条记录的 id 是 12345,那么第 3 页就该这样查:
SELECT * FROM orders WHERE id > 12345 ORDER BY id ASC LIMIT 10;
关键点:
- 必须用有序、唯一、有索引的字段(推荐主键
id或带时间戳的组合字段) - 方向要一致:
ORDER BY id ASC对应WHERE id > ?;若用DESC,就得写WHERE id - 不能跳页(比如从第 1 页直接跳到第 100 页),但对“下一页”“上一页”体验更好、更稳定
-
前端需把上一页末尾的
id值传回后端,而不是传页码
避免 SQL_CALC_FOUND_ROWS 获取总条数
老项目里常见这种写法:
SELECT SQL_CALC_FOUND_ROWS * FROM users WHERE status = 1 LIMIT 10 OFFSET 20; SELECT FOUND_ROWS();
看起来省一次查询,实际代价很高:SQL_CALC_FOUND_ROWS 会让 MySQL 扫描全表或全索引匹配行数,即使你只取 10 条。在高并发或大数据量下,这步常成瓶颈。
更务实的做法:
- 前端不强求显示“共 N 条”,只提供“下一页”按钮(游标分页天然适合)
- 如果真要总数,单独走一个轻量
COUNT(*)查询,但加缓存(比如 Redis 存 5 分钟内的统计值) - 对模糊搜索类分页(如
LIKE '%xxx%'),COUNT本身就很慢,不如前端限制最大可翻页数(如最多到第 100 页)
ORM 框架里的分页陷阱(以 Python SQLAlchemy 和 Node.js Knex 为例)
框架封装了分页逻辑,但底层仍是 LIMIT/OFFSET 或手动拼游标条件,容易忽略细节。
SQLAlchemy 中常见错误写法:
query.offset(20).limit(10) # 没有 .order_by(),结果不稳定
Knex 中易错点:
knex('posts').limit(10).offset(20) // 同样缺 order by另外,某些 ORM 的 paginate() 方法默认不带 COUNT,但文档没说清,导致前端以为有总页数,实际是估算或未实现——务必查清你用的版本是否真执行了 COUNT 查询。
真正要注意的,是排序字段有没有索引、偏移量是否动态计算正确、以及游标值有没有被前端篡改(比如传个负数或非数字 ID)。这些地方一松懈,分页就乱序、重复或漏数据。










