paginate() 默认会多执行一次 count(*) 查询以计算总页数,导致sql次数翻倍,且复杂条件下的count易引发全表扫描;应根据场景传入 total、启用 simple 模式或正确使用 with 预加载。

直接用 paginate() 默认行为,在带搜索、关联、多条件过滤的分页里,SQL 执行次数很容易翻倍——尤其是 COUNT 查询和主查询各来一次,还可能额外触发 N+1 关联查询。这不是配置问题,是调用方式和链式时机没对齐。
为什么 paginate() 会多查一次 COUNT?
ThinkPHP 的 paginate() 在默认模式下,必须先执行一条 COUNT(*) 查询来算总页数,再执行带 LIMIT 的主查询。当 WHERE 条件复杂(比如含子查询、JOIN 或函数表达式),这条 COUNT 往往无法走索引,甚至被优化器放弃使用覆盖索引,导致全表扫描。
- 常见现象:
SELECT COUNT(*) FROM user WHERE status = 1 AND created_at > '2025-01-01'耗时 300ms,而主查询只要 20ms - 如果你已经知道总数(比如缓存了或业务允许估算),就别让它查——用数组参数传
'total' => 12345 - 如果根本不需要总页数(如无限滚动、只显示“下一页”),启用
'simple' => true,它只查LIMIT $list_rows + 1判断是否有下一页,跳过 COUNT
appends() 不生效?检查 paginate() 调用顺序
appends() 是链式方法,但它只能作用于已生成的分页对象;如果在 paginate() 之后才调用,且之前已执行过 COUNT 和主查询,那补参只影响渲染链接,不影响后续翻页时的条件还原——尤其在 API 场景下容易丢参数。
- 错误写法:
$list = Db::name('user')->where($cond)->paginate(10)->appends(input('param.'))——input('param.')可能包含page,造成 URL 参数重复 - 正确做法:改用数组参数直接注入,
->paginate(['list_rows' => 10, 'query' => input('only', ['keyword', 'status'])]),自动过滤空值和 page 类参数 - 注意:
appends()对simple分页无效,因为后者不依赖总页数,也不生成数字页码链接
关联数据 + 分页 = N+1?预加载必须在 paginate() 前完成
很多人把 with() 放在 paginate() 后面,比如 $users->with('profile')->items(),这会导致每条用户记录都单独查一次 profile 表——paginate() 返回的是分页对象,不是集合,with() 对它无效。
- 必须写成:
Db::name('user')->with('profile')->where($cond)->paginate(10) - 若关联条件动态(如只查已审核的 profile),用闭包:
->with(['profile' => function ($q) { $q->where('verified', 1); }]) - 更激进的优化:把关联字段冗余到主表(如
user.profile_name),避免 JOIN,尤其在千万级用户表上效果显著
最常被忽略的一点:paginate() 的数组参数里,'page' 和 'list_rows' 是强制覆盖请求参数的,但 'var_page' 必须显式指定才能适配前端传的 page_no 或 current 这类非标准字段——漏掉它,翻页永远卡在第 1 页。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











