必须带确定性order by,否则mysql 8.0+优化器可能重排结果导致分页重复或丢失;唯一解法是order by后追加唯一非空字段(如id)确保排序稳定,且优先用游标分页替代offset。

分页查询必须带确定性 ORDER BY
不加 ORDER BY 的分页查询,MySQL 8.0+ 优化器可能重排结果,哪怕有索引;翻页时同一条记录可能出现在第 1 页和第 2 页,甚至消失。这不是 Bug,是 SQL 语义决定的。
- 强制使用主键或唯一时间戳排序,例如
ORDER BY id ASC或ORDER BY created_at DESC, id DESC - 禁用
ORDER BY RAND()、ORDER BY status等非唯一字段——它们会让LIMIT+OFFSET跳行或重复 - 若业务要求按热度排序,必须固化值(如加
hot_score列并定时更新),不能每次查都ORDER BY COUNT(*)
优先用游标分页替代 OFFSET
LIMIT 20 OFFSET 40 是“跳过前 40 行再取 20 行”,但中间若有删/插,第 3 页就漏数据或重复——这在高并发下极常见,且无法靠应用层锁解决。
- 改用游标:上一页最后一条的
id作为下一页起点,例如WHERE id > 12345 ORDER BY id ASC LIMIT 20 - 游标字段必须有索引,否则性能暴跌;禁止前端传入未校验的游标值(服务端需查一次该
id是否真实存在且属当前用户) - 后台管理等必须用
OFFSET的场景,应限制最大page(如max(1, min(100, (int)$_GET['page']))),并监控慢查询中LIMIT后数字 > 1000 的请求
分页参数必须严格校验与绑定上下文
用户传 ?page=-1&size=9999999 不仅可能越权读取全表,还可能触发 MySQL 全表扫描+内存溢出(OFFSET 越大,MySQL 越要扫描前面所有行)。
- 强制转换并约束范围:
$page = max(1, (int)$_GET['page']),$size = min(100, max(1, (int)$_GET['size'])) - 敏感接口(如订单列表)必须在分页前做权限检查,不能只靠前端隐藏链接
- 缓存分页结果时,必须绑定排序边界值(如
cache_key = "posts_cursor_12345_size20"),而非简单缓存"posts_page3"——否则新数据插入后缓存就脏了
并发翻页本身不引发竞态,但翻页逻辑嵌套写操作时极易出错
翻页只是读,不会直接导致数据不一致;但很多业务把“翻页 + 默认项切换”“翻页 + 批量操作”耦合在一起,这时真正的风险来自写逻辑的原子性缺失。
- 例如“设为默认卡”若先
UPDATE cards SET is_default = 0 WHERE user_id = ?再UPDATE cards SET is_default = 1 WHERE id = ?,并发时两张卡会同时为1 - 这类场景必须用事务 + 行锁:
SELECT ... FOR UPDATE锁住用户所有卡片,或用单条语句原子更新:UPDATE cards SET is_default = (id = ?) WHERE user_id = ? - 事务内避免长耗时操作;所有表必须用 InnoDB;PDO 必须开启
PDO::ATTR_ERRMODE_EXCEPTION保证异常触发回滚
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











