ThinkPHP5分页性能优化需禁用COUNT统计、添加复合索引、改用游标分页;关闭总数统计可跳过全表COUNT,联合索引提升WHERE+ORDER查询效率,游标分页避免大OFFSET扫描开销。

如果您在 ThinkPHP5 应用中遭遇分页响应缓慢、页面加载卡顿或数据库 CPU 占用飙升,很可能是由于 paginate() 默认触发全表 COUNT 和大 OFFSET 查询所致。以下是针对 ThinkPHP5 分页查询性能问题的多种优化路径:
一、禁用自动总数统计并改用轻量分页
ThinkPHP5 的 paginate() 默认执行 SELECT COUNT(*) 与 SELECT ... LIMIT 两条 SQL,当数据量超过十万级时,COUNT(*) 可能成为核心瓶颈。关闭总数统计可跳过 COUNT 查询,仅保留数据拉取逻辑,显著降低首屏延迟。
1、在控制器中调用 paginate 方法时,将第二个参数设为 false:
$list = Db::name('article')->where('status', 1)->paginate(15, false);
2、若需保留简单分页样式(上一页/下一页),可启用 simple 模式:
$list = Db::name('article')->where('status', 1)->paginate(15, ['simple' => true]);
立即学习“PHP免费学习笔记(深入)”;
3、为保持 URL 参数一致性(如搜索关键词),在视图中渲染时追加查询参数:
{$list->appends(request()->param())->render()}
二、为分页字段添加高效数据库索引
分页性能不仅取决于 PHP 层逻辑,更直接受限于数据库能否快速定位起始行。若 WHERE 条件与 ORDER BY 字段未命中索引,MySQL 将被迫全表扫描,即使只取 10 条数据,也可能耗时数百毫秒。
1、确认当前分页所依赖的排序字段(如 id、create_time)是否已设为 NOT NULL 并拥有索引:
SHOW INDEX FROM article WHERE Column_name IN ('id', 'create_time');
2、对复合查询条件建立联合索引,顺序须与 where + order 一致,例如:
ALTER TABLE article ADD INDEX idx_status_ctime (status, create_time DESC);
3、使用 EXPLAIN 验证查询是否走索引:
EXPLAIN SELECT * FROM article WHERE status = 1 ORDER BY create_time DESC LIMIT 10;,检查 type 字段不为 ALL,key 字段显示实际使用的索引名。
三、改用基于主键的游标分页替代 offset 分页
当用户需浏览深度页码(如第 5000 页),传统 LIMIT 100000, 20 会导致 MySQL 扫描并丢弃前 10 万行,IO 与 CPU 开销呈线性增长。游标分页通过记录上一页末尾主键值作为下一页起点,使每次查询始终走主键索引,响应时间稳定在毫秒级。
1、前端首次请求不带游标参数,后端按主键降序取第一页:
$list = Db::name('article')->where('status', 1)->order('id DESC')->limit(20)->select();
2、返回结果中提取最后一条记录的 id(如 $list[19]['id']),作为 next_cursor 值传给前端;后续请求携带该值:
$cursor = input('cursor/d', 0);
3、构造游标查询(严格匹配 order 方向):
$list = Db::name('article')<br> ->where('id', '<', $cursor)<br> ->where('status', 1)<br> ->order('id DESC')<br> ->limit(20)<br> ->select();
四、缓存分页总数以规避重复 count
若业务场景必须展示总页数(如“共 2346 页”),但又无法承受每次请求都执行 COUNT(*),可将总数缓存至 Redis,并设定合理过期策略,避免频繁穿透到数据库。
1、在数据变更频率较低的列表页,首次访问时计算总数并写入缓存:
Cache::set('article_total_count', Db::name('article')->where('status', 1)->count(), 3600);
2、分页查询时从缓存读取总数,传入 paginate 第三个参数:
$total = Cache::get('article_total_count', 0);<br> $list = Db::name('article')->where('status', 1)->paginate(15, false, ['total' => $total]);
3、在文章发布、删除等操作后,主动更新缓存:
Cache::set('article_total_count', Db::name('article')->where('status', 1)->count(), 3600);
五、使用覆盖索引优化 count 查询
当无法完全禁用总数统计,且表数据量极大时,可通过覆盖索引将 COUNT(*) 转换为 COUNT(索引列),大幅减少磁盘 I/O。前提是该字段非空、有索引,且满足查询条件过滤后仍具代表性。
1、为常用查询条件字段创建单列或联合索引,确保其具备“非空+高选择性”特征,例如:
ALTER TABLE article ADD INDEX idx_status_id (status, id);
2、改写 count 查询为只统计索引列:
Db::name('article')->where('status', 1)->count('id');
3、在 paginate 中手动注入该 count 值(需先获取):
$count = Db::name('article')->where('status', 1)->count('id');<br> $list = Db::name('article')->where('status', 1)->paginate(15, false, ['total' => $count]);



















