mysql千万级分页慢的根本原因是limit offset, size需扫描并丢弃前offset行,即使phpenv本地环境配置再优也无法改变该底层执行逻辑;应改用主键游标分页(如where id > last_id order by id limit 20)替代offset,将复杂度从o(n)降至o(log n)。

phpEnv 是本地开发环境套件,本质仍是 Apache + PHP + MySQL 组合,MySQL 千万级分页的瓶颈不在 phpEnv 本身,而在 LIMIT offset, size 的底层执行逻辑——它会强制 MySQL 扫描并丢弃前 offset 行。哪怕你只取 20 条,偏移量到 100 万时,MySQL 仍得定位到第 1000020 行,中间全走索引树遍历,I/O 和 CPU 都扛不住。
为什么 LIMIT 1000000, 20 在 phpEnv 里也慢得离谱
phpEnv 默认用的是 InnoDB 引擎,而 InnoDB 的聚簇索引决定了:主键有序时,LIMIT 才能靠索引“跳转”;一旦涉及大 offset,优化器无法跳过中间节点,只能逐页扫描 B+ 树叶子节点。你在 phpEnv 里看到查询耗时从 20ms 涨到 3.8s,不是环境问题,是 MySQL 自身机制使然。
- phpEnv 的 MySQL 版本通常为 5.7 或 8.0,默认配置未针对大表优化(如
innodb_buffer_pool_size可能仅 128MB),缓存命中率低,加剧磁盘 I/O - 本地 SSD 性能再好,也救不了扫描 100 万行主键再丢弃的逻辑浪费
-
EXPLAIN显示rows值 ≈offset + size,这就是真实扫描量,不是估算
用主键游标分页替代 OFFSET(thinkPHP 实操)
核心思路:不依赖行号,改用上一页最后一条记录的主键值作为下一页起点。这完全规避了 OFFSET,让查询始终落在索引最右位置,复杂度从 O(N) 降到 O(log N)。
- 首次请求:用
ORDER BY id ASC LIMIT 20拿头 20 条,记下第 20 条的id(比如123456) - 下一页请求:写成
WHERE id > 123456 ORDER BY id ASC LIMIT 20,而不是LIMIT 20, 20 - thinkPHP 中可这样构造:
$lastId = input('last_id', 0, 'intval'); $list = Db::name('order')->where('id', '>', $lastId)->order('id ASC')->limit(20)->select(); - 注意必须有
ORDER BY id且方向固定(ASC/DESC 要和游标值匹配),否则结果错乱或漏数据
复合条件分页时如何加索引才有效
如果分页带筛选(如 WHERE status = 1 AND created_at > '2025-01-01'),游标法仍可用,但索引设计决定成败。不能只给 status 加单列索引,那样 created_at 还是全表扫。
- 建联合索引优先按「等值字段 + 范围字段 + 排序字段」顺序:例如
INDEX idx_status_time_id (status, created_at, id) - 查询语句必须严格匹配索引前缀:
WHERE status = 1 AND created_at > '2025-01-01' ORDER BY id才能用上全部三列 - 若用
ORDER BY created_at,那索引就得是(status, created_at),但此时游标字段必须是created_at(需处理时间重复问题,比如加id作第二排序) - 在 phpEnv 的 phpMyAdmin 里用
EXPLAIN看key_len是否等于索引总长度,确认是否走了完整索引
phpEnv 下容易被忽略的三个硬伤
本地环境调试时,有些坑只有上线前夜才暴露:
- phpEnv 的 MySQL 默认关闭
slow_query_log,你根本不知道哪条分页 SQL 已经超 2s,建议手动打开并在my.ini加long_query_time = 1 - thinkPHP 的
paginate()方法默认走LIMIT OFFSET,即使你传了last_id它也不认——必须手写原生分页逻辑,别偷懒 - phpEnv 的 Apache 默认
MaxRequestWorkers很小(常为 150),并发跑 10 个深度分页请求就可能触发服务拒绝,这不是数据库问题,但现象一模一样
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











