limit offset, size在大数据量下会变慢,因为mysql需逐行扫描并丢弃前offset行,无法跳转定位,导致i/o和cpu压力随offset线性增长;应改用基于排序字段的游标分页(如where id > ?)替代。

偏移量超过 10 万后,LIMIT offset, size 查询基本不可用——MySQL 必须扫描并丢弃前 offset 行,CPU 和 I/O 压力陡增,PHP 进程常因超时中断。
为什么 LIMIT offset, size 在大数据量下会变慢
MySQL 执行 LIMIT 99900, 20 时,并不会“跳到第 99900 行”,而是从头开始逐行扫描、计数、过滤,直到凑够 99900 条才开始取后面 20 条。即使有索引,只要 ORDER BY 字段和 WHERE 条件无法被同一复合索引覆盖,优化器仍可能退化为全索引扫描。
- EXPLAIN 显示
type = index或rows接近表总行数,就是典型征兆 - 并发稍高时,这类查询容易堆积,拖垮整个数据库连接池
- PHP 层做再多缓存或异步也无济于事,瓶颈在 SQL 执行阶段
改用 WHERE id > ? 游标分页(推荐首选)
前提是排序字段唯一、非空、有索引(如自增 id 或带索引的 created_at),且只用于“下一页”连续翻页场景。
- 第一页查:
SELECT * FROM posts ORDER BY id ASC LIMIT 20,拿到最后一条的id = 12345 - 下一页查:
SELECT * FROM posts WHERE id > 12345 ORDER BY id ASC LIMIT 20 - 上一页需反向查:
SELECT * FROM posts WHERE id ,结果再用 PHP <code>array_reverse()调整顺序 - 游标值必须 URL-safe 编码(如 base64_encode),避免直接暴露主键或时间戳精度
延迟关联优化深分页(兼容跳页需求)
当业务强制要求支持「跳转任意页码」(如后台导出第 5000 页),又无法改造成游标模式时,用两层查询减少回表开销:
- 内层只查主键:
SELECT id FROM posts ORDER BY created_at DESC LIMIT 99900, 20(仍慢,但只返回整数,IO 少) - 外层用这些 ID 回表:
SELECT * FROM posts WHERE id IN (123, 456, ...) - 更稳妥写法是 JOIN:
SELECT p.* FROM posts p INNER JOIN (内层子查询) tmp ON p.id = tmp.id - 注意:该方式在 MySQL 5.7+ 效果明显,但若子查询返回 ID 数量过大(如 > 1000),
IN可能退化,此时应分批处理
COUNT(*) 总数查询必须绕开
对千万级表执行 SELECT COUNT(*) FROM huge_table 不是慢的问题,是根本不能放在线上接口里跑——它可能锁表、阻塞写入、触发慢日志告警。
- 前端只需知道“是否有下一页”:改用
SELECT 1 FROM table WHERE ... ORDER BY id LIMIT 21,查到 21 条就说明还有下一页,否则没有 - 总数显示可降级为估算值:读
SHOW TABLE STATUS LIKE 'table_name'的table_rows(InnoDB 是粗略值,误差 ±10%) - 严格需要精确总数的场景,必须用单独计数表或 Redis,由写操作(INSERT/DELETE)同步维护,而非每次查
游标分页看似限制了跳页能力,但真实用户极少手动翻到第 5000 页;而爬虫或导出类请求,本就不该走 Web 分页接口——应单独提供流式导出 API,用游标 + 持久化 cursor token 处理。最容易被忽略的是:游标值未校验合法性(比如传入负数 id 或非法 base64),会导致空结果或 SQL 报错,必须在 PHP 层 decode 后做基础类型与范围检查。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











