MySQL分页查询深分页时,因“扫描并丢弃”机制导致性能急剧下降。通过子查询先捞主键ID再JOIN详情,并利用覆盖索引,可显著提升效率。ThinkPHP5.1中应封装游标查询替代默认分页,但需放弃任意跳页功能。
首先需要明确一个核心问题:MySQL 使用 LIMIT offset, size 进行深分页时,性能会急剧下降,根本原因在于其“扫描并丢弃”的执行机制。在 ThinkPHP 5.1 中,这一现象尤为突出,因为框架默认的 paginate() 方法生成的正是此类 SQL。当数据量超过百万行,翻页至 500 页之后,超时几乎不可避免。

长期稳定更新的攒劲资源: >>>点此立即查看<<<
当 MySQL 执行 LIMIT offset, size 时,并不会直接跳转到第 offset 行,而是从索引头部开始逐行扫描、计数,跳过前 offset 条记录后,再取出 size 条。即使只需要 10 条数据,LIMIT 5000000, 10 也需要扫描并丢弃 500 万行,这造成了 IO 和 CPU 的双重浪费。在 ThinkPHP 5.1 中,paginate() 方法默认生成的正是这种 SQL。数据量一旦超过百万,首页可能还能快速响应,但翻到第 500 页时极易卡死。
核心思路是将“扫描全表找数据”分解为两步:首先利用主键(或有序索引字段)快速定位目标 ID,然后仅查询这些 ID 对应的完整记录。MySQL 可以直接走主键索引,跳过中间所有无效行。
id)是主键或拥有单列/复合索引,并且 ORDER BY 的方向与索引顺序一致(例如 ORDER BY id DESC 对应 INDEX idx_id (id DESC))SELECT t.* FROM article t INNER JOIN ( SELECT id FROM article WHERE status = 1 ORDER BY id DESC LIMIT 500000, 10 ) tmp ON t.id = tmp.id;
Db::query() 或封装模型方法来调用该逻辑,以避免 ORM 自动注入带来的干扰子查询 SELECT id FROM ... 如果能完全利用索引而不回表,性能将进一步提升。主键本身是聚簇索引,天然满足这一条件;但如果按 create_time 分页,则需要建立覆盖索引,例如:ALTER TABLE article ADD INDEX idx_status_ctime_id (status, create_time DESC, id);
WHERE status = ORDER BY create_time DESC 直接定位,且因为 id 字段已包含在索引中,子查询无需访问主表数据页EXPLAIN 验证子查询是否使用了该索引:检查 type 是否为 range 或 ref,key 是否显示索引名,以及 Extra 是否不包含 Using filesort 或 Using temporarystatus)必须放在前面,范围/排序字段(create_time)紧随其后,被 SELECT 的 id 放在最后——这是联合索引生效的关键避免在控制器中直接嵌入原生 SQL,也不要强行给 paginate() 打补丁。最稳妥的方式是封装一个轻量级的游标查询方法,替代默认的分页入口。
public function cursorPage($lastId = 0, $size = 15, $where = []) { $query = $this->where($where); if ($lastId > 0) { $query = $query->where('id', '>', $lastId); } return $query->order('id ASC')->limit($size)->select(); }last_id=100500 参数,后端直接调用,无需 COUNT 和 OFFSET,响应时间稳定在毫秒级别NOT NULL;ORDER BY 和 WHERE 条件必须能命中同一索引;前端必须放弃「跳转任意页码」功能,改为使用「下一页」按钮驱动真正困难的并非写出正确的 SQL,而是说服产品团队接受「不支持跳页」的设计——因为深分页本身是一个伪需求,用户极少真正翻到第 1000 页,强制支持只会拖垮整个数据库。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述