distinct()在关联查询中常失效,因其默认对join后整行字段去重,主表相同记录因子表字段不同被视为不重复;需用select指定字段、子查询先去重或groupby+聚合等方案解决。

distinct() 在关联查询中为什么经常失效
因为 distinct() 作用于整行 SELECT 字段,而关联查询(如 join())通常会引入子表字段,导致原本相同的主表记录因子表数据不同而被判定为“不重复”。比如查订单 + 关联门票,即使同一个 ord_id 出现多次,只要关联的 exhibit_sn 不同,distinct() 就不会合并它们。
常见错误现象:Order::join('order_item', 'orders.ord_id', '=', 'order_item.ord_id')->distinct()->get() 仍返回多条相同 ord_id 记录。
- 必须显式用
select('orders.ord_id')限定去重字段,否则默认对所有 JOIN 后字段组合去重 - 若后续还要查
order_sn等主表字段,得确保这些字段在所有匹配行中值一致,否则仍无法压缩行数 - Laravel 10.28+ 支持
distinct('ord_id')单字段语法,但低版本不兼容,需谨慎使用
用子查询先去重再 JOIN 是最稳的写法
把去重逻辑下推到子查询层,让数据库先拿到唯一主键,再回主表取完整数据。这绕过了 JOIN 引入的冗余维度,也兼容分页。
实操建议:
- 用
DB::raw("(SELECT DISTINCT ord_id FROM order_item WHERE type = 1) AS item_ids")构建子查询别名 - JOIN 时用
->join(DB::raw(...), 'orders.ord_id', '=', 'item_ids.ord_id') - 之后可自由加
where()、orderBy()和paginate(15),不会破坏去重逻辑 - 避免手拼 SQL 字符串;用
DB::raw()包裹子查询即可保持 Query Builder 链式调用
groupBy 配合聚合或 latest() 取最新一条
当你要的不是任意一条去重记录,而是每个分组里“最新/最高优先级”的那条(例如每个 user_id 对应最新一篇博客),distinct() 不够用,groupBy() 才是正解。
注意点:
- MySQL 8.0+ 严格模式下,
select('user_id', 'title', 'created_at')+groupBy('user_id')会报错,除非title和created_at是聚合字段或也在 GROUP BY 中 - 安全写法是:先
groupBy('user_id'),再用latest('created_at')或orderByDesc('created_at'),最后first()—— 但要注意这是 Eloquent Collection 层行为,大数据量慎用 - 更可靠的是窗口函数:用
DB::raw("ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) as rn")+ 外层筛选rn = 1
一对多查询后 PHP 层 unique() 是性能陷阱
collect($orders)->unique('ord_id') 看似简单,但它要求先把全部关联结果加载进 PHP 内存。一旦子表数据量大(比如一个订单关联上百张门票),内存和网络开销会陡增。
真正该做的:
- 拒绝
->with('items')->get()->unique('ord_id')这类链式写法 - 把去重压力留在数据库:要么子查询,要么
GROUP BY+ 聚合,要么用 Laravel 11 的hasMorphOne类型关系替代硬 JOIN - 如果业务允许只查 ID 列表,优先走
Order::join(...)->distinct()->pluck('ord_id'),它生成SELECT DISTINCT orders.ord_id,最轻量
复杂点在于:去重目标到底是“主表记录唯一”,还是“主表 + 某个业务维度唯一”——前者用子查询,后者往往得靠业务字段参与分组或窗口计算。别指望一个 distinct() 调用覆盖所有场景。











