withAvg() 是最轻量方式查询关联表字段平均值,仅添加如 posts_avg_views 的数字属性,不加载关联数据;支持自定义字段名但仅限一级关联,多聚合或过滤需用 selectRaw()+JOIN 或子查询。

用 withAvg() 直接查关联表平均值
想在查主模型时顺带拿到关联表某个字段的平均值,withAvg() 是最轻量、最符合直觉的方式。它不会加载关联记录本身,只执行一个子查询或 JOIN + AVG 聚合,性能比 with() + 手动计算好得多。
常见错误是把它当 with() 用,以为会返回关联模型数据——其实它只往主模型实例上挂一个形如 avg_rating 的属性(名称可自定义),且该属性是数字或 null。
-
User::withAvg('posts', 'views')->get()会给每个User实例加一个posts_avg_views属性(默认命名规则) - 想自定义字段名,用数组语法:
withAvg(['posts' => 'avg_views'], 'views') - 注意:关联关系必须已定义,且数据库字段存在;如果某用户没有 posts,
avg_views为null,不是 0 - 不支持多层嵌套关联(比如
user.posts.comments),只能一级关联
需要同时查多个聚合值?用 selectRaw() + leftJoin()
当你要的不只是平均值,还想要关联表的计数、最大值、甚至主表和关联表字段混算(比如“用户发帖平均阅读量”),withAvg() 就不够用了。这时候得手动写 JOIN 和聚合,selectRaw() 是最可控的方式。
容易踩的坑是忘记 groupBy() —— 没它的话,聚合结果会错乱,MySQL 8.0+ 甚至直接报错 Expression #1 of SELECT list is not in GROUP BY clause。
- 示例:查每个用户的帖子平均阅读量和总评论数
User::select('users.*')
->leftJoin('posts', 'users.id', '=', 'posts.user_id')
->selectRaw('AVG(posts.views) as avg_views')
->selectRaw('COUNT(comments.id) as total_comments')
->leftJoin('comments', 'posts.id', '=', 'comments.post_id')
->groupBy('users.id')
->get();
groupBy('users.id') 必须包含所有非聚合字段;若用 select('*'),需明确列出 users 表所有字段,否则可能出错avg_views 为 null),用 INNER JOIN 则只返回有数据的用户
has() 或 whereDoesntHave() 配合平均值筛选
有时候你不是要展示平均值,而是要用它做条件过滤——比如“只查平均评分 ≥ 4.5 的商品”。这时候不能靠 withAvg(),因为它只是附加数据,不参与 WHERE。
正确做法是用子查询或 JOIN 做聚合过滤,Laravel 提供了 whereHas() 的增强版:whereHasMorph() 不适用,但可以用 whereHas() 搭配闭包里的 selectRaw() + having()。
- 推荐写法(子查询):
Product::whereExists(function ($query) {
$query->selectRaw(1)
->from('reviews')
->whereColumn('reviews.product_id', 'products.id')
->groupBy('reviews.product_id')
->havingRaw('AVG(reviews.rating) >= ?', [4.5]);
})->get();
having()(需确保主表字段在 GROUP BY 中)whereHas() 闭包里直接写 avg() —— Laravel 不会自动把它转成 HAVING,会当成 WHERE 处理,导致 SQL 错误关联平均值字段在 Eloquent 访问时的陷阱
用 withAvg() 或 selectRaw() 加的平均值字段,看起来像模型属性,但它们不是真正意义上的 Eloquent 属性——不走 $casts、不触发 getAttribute()、也不能直接用于 toArray() 之外的序列化逻辑(比如 API Resource 中未显式添加会丢失)。
- 如果你在 Resource 里写
$this->avg_views,它能取到值;但写$this->getOriginal('avg_views')会返回null,因为不是原始数据库字段 - 想统一处理这类计算字段,建议在模型中加访问器:
protected $appends = ['avg_rating'];
public function getAvgRatingAttribute()
{
return $this->attributes['posts_avg_rating'] ?? 0;
}
withAvg() 生成的属性名要对齐;否则取不到值selectRaw(),字段名是你自己写的,访问器可以直接对应,不用依赖 Laravel 的默认命名逻辑


















