传统offset分页在页码较深时性能骤降,因数据库需扫描并丢弃大量前置行;应优先使用游标分页(基于有序唯一索引字段的范围查询),避免随机跳页场景下的性能瓶颈。
mysql 用 limit offset 最直接,但页码一深就卡
查一万条数据,用户只看前几页?那就别让数据库全算出来。用 limit 和 offset 是最直白的解法,比如每页 20 条、看第 5 页:select * from orders order by created_at desc limit 20 offset 80(因为 (5−1)×20 = 80)。
但注意:OFFSET 越大,数据库越可能扫完整个排序结果再丢掉前面的行——第 1000 页(OFFSET 19980)时,性能会明显下滑。
- 必须带
ORDER BY,否则分页结果不一致(尤其有并发写入时) -
LIMIT 20 OFFSET 80和LIMIT 80, 20效果一样,但后者是 MySQL 特有语法,PostgreSQL 不认 - 如果表没索引在
ORDER BY字段上,排序本身就会变慢,分页只是放大了问题
大数据量别硬跳 OFFSET,改用游标分页(Cursor-based Pagination)
当页码动辄上百、或实时性要求高(比如日志流、消息列表),传统 OFFSET 分页就该换了。核心思路是:不记“第几页”,而记“从哪条继续”。比如上一页最后一条记录的 id 是 12345,下一页就查:SELECT * FROM events WHERE id > 12345 ORDER BY id ASC LIMIT 20。
这叫游标分页,本质是把分页状态外移到应用层,数据库只做范围扫描。
- 依赖字段必须有序、唯一、有索引(通常是主键或时间戳)
- 不能跳转任意页(比如直接点“第 87 页”),但对“加载更多”场景更稳更快
- 要注意边界值:若用时间戳,同一秒有多条记录,需加第二排序字段防漏/重
PHP 或 Java 后端拼 SQL 时,offset 值别手算错
前端传 page=3&size=15,后端算 offset 得是 (3 − 1) × 15 = 30,不是 3 × 15。这个小错误会导致每页少显示一页数据,而且很难一眼发现。
- 推荐封装一个计算函数,比如 PHP 的
getOffset($page, $size),避免散落在多处重复计算 - 务必校验
$page和$size是正整数,防止注入或负数 offset 导致意外行为 - Java JDBC 中,用
PreparedStatement绑定参数,别字符串拼接OFFSET ?——既防 SQL 注入,也避免类型转换出错
查总数 COUNT(*) 和分页查数据,别在同一条 SQL 里硬凑
有些同学想“一次查完”,写成:SELECT *, COUNT(*) OVER() AS total FROM table LIMIT 20 OFFSET 0。看起来省事,实际多数场景反而更慢——窗口函数要先算全量再截断,没省下扫描成本。
真要总数,单独走一次 COUNT(*);真要分页数据,按需查。两者可并行,也可缓存总数(比如总数不变时,前端翻页就不重查)。
- 如果表超大且总数不常变,用
SELECT TABLE_ROWS FROM information_schema.TABLES查近似值,快但不准 - 某些 ORM(如 Laravel 的
paginate()、MyBatis-Plus 的Page)会自动拆成两条语句,要看清它生成的 SQL 是不是真做了优化 - 别为了“减少查询次数”牺牲可读性和可维护性——两次简单查询,比一条复杂窗口函数更易调试和索引优化
分页看着简单,真正上线后卡顿、跳页错位、总数不准,往往不是语法写错了,而是没想清楚:你要的是“随机跳页”,还是“顺序加载”;数据是静态的,还是每秒都在写入;数据库有没有为排序字段建好索引。这些细节,比记住 LIMIT 怎么写重要得多。










