根本原因是paginate()在with()场景下对主表COUNT而实际返回JOIN后的多行,导致分页切片错位;必须改用显式join()确保单表逻辑,或先聚合再关联。

为什么关联查询分页会出重复数据
根本原因不是 SQL 写错,而是 paginate() 在 with() 场景下无法对 JOIN 后的结果集做一致 COUNT 和 LIMIT —— 它默认对主表 COUNT,但实际返回的是 LEFT JOIN 后的多行(比如 1 个用户带 3 条订单,就生成 3 行),导致分页切片错位。
常见错误现象:
-
$list->total()返回 500,但count($list)只有 120 条记录 - 第 1 页显示用户 A 的 3 条订单,第 2 页突然跳过用户 B(本该出现),却重复出现用户 C
- 开启 debug 后看到两条 SQL:
SELECT COUNT(*) FROM user和SELECT * FROM user LEFT JOIN order ... LIMIT 0,15,基数天然不等
用 join() 替代 with() 确保单行语义
只要分页目标是“每个用户一行 + 关联字段”,就必须让主查询变成单表逻辑,不能依赖预载入。否则 with() 会在分页后为每条结果单独查关联表,N+1 且总数失真。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 改写为显式
join(),例如:Db::name('user')->alias('u')->join('order o', 'u.id = o.user_id AND o.is_latest = 1')->field('u.*, o.order_no, o.amount')->paginate(15) - 如果要取「每个用户的最新一笔订单」,先用子查询兜底:
$sub = Db::name('order')->field('user_id, MAX(create_time) as max_time')->group('user_id'),再join([$sub => 'latest'], 'u.id = latest.user_id') - 避免在
join()后还调with('xxx')—— 这会触发二次查询,彻底破坏分页一致性
union 或 group 分页时重复数据怎么防
union() 默认去重整行,group() 要求字段严格匹配,稍有不慎就会漏数据或报错。尤其当合并分表、或按某字段聚合取最新时,很容易因语义不明确导致重复。
关键注意点:
-
union()子查询必须field()字段数、顺序、别名完全一致,否则 MySQL 报错或结果错乱 - 用
group('user_id')时,MySQL 5.7+ 默认开启ONLY_FULL_GROUP_BY,直接select *会失败;应只选必要字段,或用聚合函数包裹,如:field('user_id, MAX(order_amount) as amount, MAX(create_time) as latest') - 不要指望
distinct(true)->order('id desc')->limit(1)取最新 —— ThinkPHP 会把limit放在外层,去重前顺序已不可控
游标分页能绕开重复问题吗
不能直接解决重复,但能规避由 offset 引发的错位放大效应。当主键/时间戳有序且有索引时,游标分页通过 where('id', '>', $lastId) 拿下一页,天然避免了跨页重复或跳漏。
但前提是:你得先确保单次查询结果本身无重复 —— 如果 JOIN 或 UNION 逻辑本身已产生重复行,游标只是稳定地翻这些错的数据。
所以真正容易被忽略的一点是:重复数据问题永远根植于查询构造阶段,而不是分页方式本身;paginate 只是把底层逻辑不一致的问题暴露得更明显而已。



















