Laravel模型查询去重,distinct方法如何高效使用?
- 内容介绍
- 文章标签
- 相关推荐
本文共计1055个文字,预计阅读时间需要5分钟。
在调用`select()`之后链式使用`distinct()`,但结果仍然是重复的——这大概是因为你没有理解Laravel的`distinct()`实际作用的原理。实际上,`distinct()`实现的不是某个字段的去重,而是整体结果的去重。这意味着它等价于SQL中的`DISTINCT *`,即只要求任意一列的值不重复即可,而不是要求所有列的值都不重复。
简单来说,如果你希望结果不包含重复的行,你需要指定一个或多个字段进行去重。例如,如果你有一个用户表,你想获取不重复的用户ID,可以这样写:
比如查用户邮箱和头像: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() 就成了幻觉。
本文共计1055个文字,预计阅读时间需要5分钟。
在调用`select()`之后链式使用`distinct()`,但结果仍然是重复的——这大概是因为你没有理解Laravel的`distinct()`实际作用的原理。实际上,`distinct()`实现的不是某个字段的去重,而是整体结果的去重。这意味着它等价于SQL中的`DISTINCT *`,即只要求任意一列的值不重复即可,而不是要求所有列的值都不重复。
简单来说,如果你希望结果不包含重复的行,你需要指定一个或多个字段进行去重。例如,如果你有一个用户表,你想获取不重复的用户ID,可以这样写:
比如查用户邮箱和头像: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() 就成了幻觉。

