结论:withjoin联查后直接调用paginate()会导致分页总数不准、当前页数据为空,因count(*)未同步join条件;必须显式传入countquery参数并确保count(distinct 主表.id)。

ThinkPHP 6 的 withJoin 联查 + paginate 会丢数据?
直接说结论:用 withJoin 做多表联查后调用 paginate(),默认会因 count(*) 统计逻辑错误导致分页总数不准、当前页数据为空。这不是 bug,是底层把关联字段当主表字段参与了去重和统计——尤其在一对多关联时极易复现。
根本原因在于:paginate() 默认执行的 COUNT 语句不带 JOIN,但查询主体又依赖 JOIN 条件(比如 WHERE user.status = 1 AND profile.city = '深圳'),导致 count 和 select 的数据范围不一致。
- 必须显式传入
countQuery参数,让分页总数统计走带 JOIN 的完整逻辑 - 避免在
withJoin后直接链式调用paginate(),否则 ThinkPHP 不会自动同步 JOIN 到 count 查询中 - 如果用了
field(),确保 count 查询也使用相同字段(或至少包含主键),否则可能触发全表扫描或报错
正确写法:手动构造 countQuery 并复用查询条件
核心思路是把主查询的 where、join、alias 等条件“复制”一份给 count 查询,而不是依赖框架自动推导。
// 示例:查用户 + 关联头像表 + 分页
$builder = User::alias('u')
->join('user_profile p', 'u.id = p.user_id', 'LEFT')
->where('u.status', 1)
->where('p.city', '深圳');
<p>// 构造 count 查询(关键!)
$countQuery = clone $builder;
$countQuery->reset()->field('COUNT(DISTINCT u.id)')->buildSql();</p><p>// 执行分页(传入 countQuery)
$list = $builder
->field('u.id,u.name,p.avatar,p.city')
->paginate([
'list_rows' => 15,
'query' => request()->param(),
'count_query' => $countQuery, // ← 必须指定
]);
</p>
-
clone $builder是为了复用 where/join,但需调用reset()清除原有字段和 limit,否则 buildSql 会出错 -
COUNT(DISTINCT u.id)防止一对多导致重复计数(比如一个用户有 3 条 profile 记录) -
field()在主查询里必须明确,否则 paginate 可能取不到主键引发异常
ThinkPHP 5.1 怎么办?没有 count_query 参数
TP5.1 的 paginate() 不支持传入自定义 count 查询,只能绕过 ORM,改用 Db::table() 手写原生分页逻辑。
$page = input('page', 1);
$limit = 15;
$offset = ($page - 1) * $limit;
<p>// 先查总数(手写 JOIN 的 COUNT)
$total = Db::table('think_user u')
->join('think_user_profile p', 'u.id = p.user_id')
->where('u.status', 1)
->count('DISTINCT u.id');</p><p>// 再查数据
$data = Db::table('think_user u')
->join('think_user_profile p', 'u.id = p.user_id')
->where('u.status', 1)
->field('u.id,u.name,p.avatar')
->limit($offset, $limit)
->select();</p><p>// 手动构建分页对象
$pages = new \think\Paginator\Bootstrap($data, $limit, $page, $total, [
'query' => request()->param(),
]);
</p>
- TP5.1 没有
count_query,硬上withJoin+paginate必然翻车 - 注意
Bootstrap分页类路径要匹配你用的模板引擎(如Bootstrap4或Bootstrap5) - 别漏掉
query参数,否则分页链接里的 GET 参数会丢失
为什么不用 with + paginate?
with 是懒加载/预加载,走的是多次查询(N+1 或 2 查询),不是 SQL JOIN;它和 paginate 结合时,分页只作用于主表,关联数据靠 PHP 层拼装——这会导致:分页数对,但每页展示的关联数据可能为空(比如 profile 表没匹配记录),且无法在 WHERE 中过滤关联字段(如 profile.city)。
- 要用关联字段做条件 or 排序 or 分页统计 → 必须用
join或withJoin - 只要求展示关联数据、不参与条件/排序 →
with更安全简洁 -
withJoin在 TP6.0+ 才稳定,TP5.1 只有join原生写法可靠
分页总数不准这件事,往往上线后才暴露,而且只在关联数据不均匀时发生——比如大部分用户没填城市,少数填了,一查“深圳”就崩。动手前先确认你用的是 TP 版本,再决定走封装还是手写。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











