distinct() 未去重是因为它作用于整行而非单字段,只要任意列不同即视为不重复;正确按字段去重需用 groupby+聚合、distincton(仅pg)或 collection 处理。

distinct() 为什么没去重?常见写法误区
直接在 select() 后链式调用 distinct(),但结果还是重复——大概率是因为你没理解 Laravel 的 distinct() 实际作用的是「整行结果」,不是某个字段。它等价于 SQL 的 DISTINCT *,只要任意一列不同,整行就算不重复。
比如查用户邮箱和头像:User::select('email', 'avatar')->distinct()->get(),只要 email 和 avatar 组合唯一,就保留;但如果两个用户邮箱相同、头像不同,这两条都会出来——这不是 bug,是预期行为。
- 想按单字段去重?不能只靠
distinct(),得配合groupBy()或子查询 -
distinct()在 MySQL 中对 NULL 值默认视为相同,但 PostgreSQL 可能不一致,跨数据库时要留意 - 如果用了
with()预加载关联模型,distinct()通常失效——因为 JOIN 会放大主表行数,去重逻辑被破坏
Laravel 10+ 中 distinctOn() 的正确姿势
PostgreSQL 支持 DISTINCT ON,Laravel 10 起原生支持 distinctOn() 方法,但仅限 PG。它才是真正按指定字段“取第一条”的去重方式。
例如:取每个部门薪资最高的员工(只取一人):
User::select('department', 'name', 'salary')
->distinctOn('department')
->orderBy('department')
->orderByDesc('salary')
->get();
-
distinctOn()必须配合orderBy(),且第一个orderBy字段必须是distinctOn的字段(否则 PG 报错) - MySQL 用户别试这个方法——会直接抛出
SQLSTATE[42000]: Syntax error - 如果需要兼容多数据库,建议用子查询或 Collection 处理,而不是强依赖
distinctOn()
按字段去重的稳妥方案:groupBy + 最值聚合
最通用、跨库安全的做法是用 groupBy() 配合聚合函数,比如取每个邮箱的最新用户记录:
User::selectRaw('MAX(id) as id, email, MAX(created_at) as created_at')
->groupBy('email')
->get()
->map(fn ($row) => User::find($row->id));
注意:这里分两步走,先查出每组关键 ID,再查完整模型——避免 SELECT * + GROUP BY 在 MySQL 严格模式下报错。
- MySQL 5.7+ 默认开启
ONLY_FULL_GROUP_BY,直接select('*')->groupBy('email')会失败 - 如果只是要字段值(不要模型实例),用
pluck()或value()更轻量 -
groupBy()本身不保证顺序,记得显式加orderBy()控制“取哪一条”
内存去重:什么时候该交给 collect()?
当数据量不大(比如几百条以内)、逻辑复杂(涉及关联字段判断或业务规则),硬塞进 SQL 反而难读易错,不如查出来再用 Collection 去重。
例如:按用户手机号去重,但优先保留已验证的记录:
User::with('profile')->get()
->sortByDesc('is_verified')
->groupBy('phone')
->map->first();
- 别在大列表上用
unique()直接传闭包做复杂判断——PHP 内存和时间都吃紧 -
groupBy()返回的是Collection,不是数组,别误用array_unique() - 如果后续还要分页,Collection 去重后必须用
forPage()手动切片,Paginator 不识别
真正麻烦的从来不是语法,而是没想清楚“去重依据是什么”和“重复时该留谁”。字段语义模糊、NULL 值处理、数据库方言差异——这些地方一漏,distinct() 就成了幻觉。











