mysql分页必须带order by确保结果稳定,否则顺序不可靠;offset越大性能越差,超10000建议改用游标分页;limit参数不支持占位符,需intval过滤后拼接,且需限制每页条数防压垮数据库。

直接用 LIMIT 和 OFFSET 是最常用、也最容易出错的方式。关键不在“能不能查”,而在“怎么查才不崩、不慢、不被注入”。
MySQL分页必须带 ORDER BY,否则结果不可靠
没有排序的 LIMIT 查询,数据库返回顺序是不确定的。哪怕表数据没变,两次查询结果顺序也可能不同——尤其在有并发写入或 InnoDB MVCC 机制下。
- 必须显式指定
ORDER BY字段,且该字段最好有索引(比如主键id或时间戳created_at) - 避免用
SELECT *,只查需要的字段,减少网络和内存开销 - 错误写法:
SELECT * FROM posts LIMIT 10 OFFSET 20(缺ORDER BY) - 正确写法:
SELECT id, title, created_at FROM posts ORDER BY id DESC LIMIT 10 OFFSET 20
OFFSET 越大,查询越慢,10万行后明显卡顿
MySQL 执行 OFFSET 100000 时,实际会扫描前 100001 行再丢弃前面的,不是“跳过去”。数据量一上去,响应就拖慢。
- 当
OFFSET > 10000时,建议改用游标分页(cursor-based pagination) - 游标分页依赖上一页最后一条记录的排序字段值,例如:上一页最后的
id = 12345,下一页查WHERE id - 不能跳页(比如直接点第 200 页),但滚动加载、无限下拉场景更稳
- 如果必须支持跳页,至少给
ORDER BY字段加联合索引,如INDEX (status, created_at, id)
PHP里别直接拼 $_GET['page'] 到 SQL 中
用户传个 ?page=1; DROP TABLE users; 就完蛋。哪怕用了 intval(),也要防住负数、超大数、非数字等边界情况。
- 页码必须转成整型并校验范围:
$page = max(1, min($page, $total_pages)) - 更安全的做法是用预处理语句:
$stmt = $pdo->prepare("SELECT ... LIMIT ? OFFSET ?"); $stmt->execute([$limit, $offset]); - 注意:MySQL 的
LIMIT参数不支持占位符(PDO 会报错),所以$limit和$offset需先用intval()过滤后再代入,或改用字符串拼接 + 白名单校验 - 每页条数也得限制上限,比如不允许
per_page=1000,防止一次拉太多数据压垮 DB
总记录数别每次都 COUNT(*) 查一遍
分页导航要显示“共 XX 条”“共 XX 页”,就得知道总数。但对千万级表执行 COUNT(*) 可能要秒级延迟。
- 小表(SELECT COUNT(*) 最简单
- 大表:用
SQL_CALC_FOUND_ROWS(MySQL 8.0.17+ 已废弃,慎用)或缓存总数(Redis 存posts:count,配合写操作更新) - 或者前端接受“仅显示下一页”按钮,不显示总页数(很多 App 就这么干)
- 注意:
FOUND_ROWS()在含UNION或子查询时不准,别无脑信
真正难的不是写出第一版分页,而是当数据从 1 万涨到 100 万时,OFFSET 不拖慢接口、总数不卡住首页、用户输个非法页码也不报 500。这些细节藏在参数校验、索引设计和查询模式切换里,而不是 LIMIT 语法本身。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











