MySQL分页首选LIMIT+OFFSET,需配合ORDER BY及索引;OFFSET过大时性能骤降,应改用基于唯一有序字段的游标分页(如WHERE id > ? LIMIT ?)。
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,且排序字段最好有索引(如 cr
eated_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
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 条。在高并发或大数据量下,这步常成瓶颈。
更务实的做法:
COUNT(*) 查询,但加缓存(比如 Redis 存 5 分钟内的统计值)LIKE '%xxx%'),COUNT 本身就很慢,不如前端限制最大可翻页数(如最多到第 100 页)框架封装了分页逻辑,但底层仍是 LIMIT/OFFSET 或手动拼游标条件,容易忽略细节。
SQLAlchemy 中常见错误写法:
query.offset(20).limit(10) # 没有 .order_by(),结果不稳定
Knex 中易错点:
knex('posts').limit(10).offset(20) // 同样缺 order by另外,某些 ORM 的 paginate() 方法默认不带 COUNT,但文档没说清,导致前端以为有总页数,实际是估算或未实现——务必查清你用的版本是否真执行了 COUNT 查询。
真正要注意的,是排序字段有没有索引、偏移量是否动态计算正确、以及游标值有没有被前端篡改(比如传个负数或非数字 ID)。这些地方一松懈,分页就乱序、重复或漏数据。