Laravel嵌套关联查询去重有四种方法:distinct()仅适用于只选主表字段的简单去重;groupBy()用于按主键分组并聚合统计;with()预加载避免JOIN产生笛卡尔积;unique()集合去重仅限小数据量补救。

在 Laravel 中处理嵌套关联查询时,因 JOIN 多张表导致主表记录重复出现,是高频且隐蔽的性能与数据准确性陷阱——比如查用户及其订单、订单商品、商品分类,一条用户记录可能因关联出 5 个商品而被复制 5 次,后续 count、sum、map 全部错乱。
用 distinct() 治标:快速过滤重复行
当只需去重后的主表 ID 或简单字段列表,且不依赖 JOIN 后的关联字段参与计算时,这是最快路径。
执行 DB::table('users')->join('orders', 'users.id', '=', 'orders.user_id')->distinct()->get(['users.id', 'users.name'])。
这一步操作起来很简单,但【注意:distinct() 作用于整行 SELECT 列表】——如果你同时 select 了 orders.created_at,那两个不同时间的订单就会让同一 user 出现两次,去重失效。
只取唯一 ID 时更安全:DB::table('users')->join('orders', ...)->distinct()->pluck('users.id')。
用 groupBy() 治本:按主键分组并聚合
真正需要统计、求和、取最新值,或必须保留某条关联记录(如每个用户的最新订单)时,groupBy 是不可绕过的解法。
第一步:明确主表唯一标识字段,通常是 users.id;
第二步:在 SELECT 中只保留主表字段 + 聚合表达式,例如 select('users.id', 'users.name')→selectRaw('COUNT(DISTINCT orders.id) as order_count')→selectRaw('MAX(orders.total) as max_order');
第三步:GROUP BY 必须包含所有非聚合字段,写成 ->groupBy('users.id', 'users.name');
【MySQL 严格模式下缺一不可,否则直接报错】;
第四步:若需关联表中某条具体记录(如最新订单),不能靠 GROUP BY 硬取,得改用子查询或窗口函数——此步结束。
用预加载 with() 绕开 JOIN
当你本质只需要主表数据 + 关联模型的少量字段(如用户头像、订单状态),而非做跨表聚合,JOIN 就是过度设计。
直接写 User::with(['orders' => function ($q) { $q->select('id', 'user_id', 'status', 'total'); }])->get()。
这会生成两条独立 SQL:一条查 users,一条用 IN 查询 orders,彻底避开笛卡尔积;
如果还要进一步筛选(如只查有未完成订单的用户),加 whereHas('orders', fn($q) => $q->where('status', 'pending'));
这一步不会产生任何重复行,因为 orders 是作为子集合挂在每个 User 实例上,不是横向展开的列。
用集合 unique() 善后:仅限小数据量补救
方法一:对已查出的 Eloquent 集合按主键去重
执行 $users = User::with('orders')->get()->unique('id')->values();
方法二:对原始数组按复合键去重(如 user_id + category)
执行 collect($rawData)->unique(fn($item) => $item['user_id'] . '_' . $item['category']);
【严禁在大数据集上调用 get()->unique()】——10 万条记录全载入内存再 PHP 层去重,极易触发 OOM;
这一步只适用于调试、后台脚本临时修复、或数据量稳定在千级以内的管理后台导出场景。


















