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)。这些地方一松懈,分页就乱序、重复或漏数据。
