You need to enable JavaScript to run this app. Laravel Eloquent 如何在pivot关联中计算产品平均评分并规避N+1问题
优惠活动
大模型
产品
解决方案
定价
更多

Laravel Eloquent 如何在pivot关联中计算产品平均评分并规避N+1问题

原实现N+1问题根因

原代码的平均分访问器触发额外查询的核心原因是:$this->reviews()->average('rating') 属于数据库查询构造器调用,不会复用已经通过with('reviews')加载到模型内存中的关联评论数据,会针对每个产品单独执行一次聚合查询,最终查询数量为「1条产品表查询 + 1条关联评论查询 + N条单产品平均分查询」,和产品数量线性相关,数据量大时性能会快速下降。


无N+1的优化实现

方案1:基于已加载关联数据的本地计算(适配现有代码结构)

不修改原有模型关系定义,仅调整平均分访问器逻辑,从已经预加载到内存的评论集合中计算平均分,不触发任何额外SQL。

  • 修改Product模型的平均分访问器:
public function getProductRatingAttribute()
{
    // 兜底:如果关联未加载则主动触发一次加载,避免非预加载场景下的异常
    if (!$this->relationLoaded('reviews')) {
        $this->load('reviews');
    }

    // 从内存中的评论集合提取所有评分值
    $ratings = $this->reviews->pluck('pivot.rating');

    // 无评论时返回null,匹配示例输出结构
    if ($ratings->isEmpty()) {
        return null;
    }

    // 保留4位小数格式,匹配示例输出的字符串格式
    return number_format($ratings->avg(), 4, '.', '');
}
  • 简化查询逻辑,不需要额外调用append方法:
// 仅需2条固定SQL:1条查所有产品、1条批量查所有关联评论,后续计算全部在内存完成
$productList = Product::with('reviews')->get();

return response()->json(['msg' => $productList]);

该方案在所有规模的产品列表下查询数固定为2,彻底消除N+1问题。

方案2:SQL层预加载聚合(适配超大数据量场景)

如果单产品的评论量极大,全量加载评论到内存会造成内存压力,可以使用Laravel内置的预加载聚合能力,直接在SQL层计算平均分,不需要加载全量评论数据。

  • 不需要修改原有reviews关联定义,仅在查询时通过withAvg将平均分作为产品的附属字段查询出来:
// 同样仅需2条固定SQL:1条查产品(含平均分子查询)、1条批量查关联评论
$productList = Product::with('reviews')
    ->withAvg('reviews as product_rating', 'rating')
    ->get();

// 如需匹配示例的4位小数格式,可在模型中补充简单的格式化访问器
// public function getProductRatingAttribute($value)
// {
//     return is_null($value) ? null : number_format($value, 4, '.', '');
// }

return response()->json(['msg' => $productList]);

该方案不需要将全量评论数据加载到内存,适合评论量达到十万级以上的高并发场景。


验证效果

两种优化方案的最终返回结构完全匹配示例JSON:

  • 有评论的产品会输出4位小数格式的平均评分
  • 无评论的产品评分返回null,关联评论为空数组
  • 全程不会因为产品数量增长产生额外SQL查询,Telescope监控下的查询数会从原来的N+2降到固定2条。

内容的提问来源于stack exchange,提问作者Miracle Hades

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.23 15:15:05