Laravel关联查询如何求和_Laravel求和关联查询字段【技巧】
withSum()作为轻量预加载求和方案,通过左连接加SUM()实现。手动join需确保selectRaw别名与GROUPBY匹配。sum()返回单值,而withSum()为每个模型注入属性。子查询求和适用于复杂聚合或分页场景。
withSum() 是最轻量的预加载求和方案,一条 SQL 通过左连接 + SUM() 实现;手动 join 需注意 selectRaw 别名与 GROUP BY 匹配;sum() 返回单值,withSum() 为每模型注入属性;子查询求和仅适用于复杂聚合或分页场景。

先说几个关键判断。说到预加载求和,withSum() 算得上是当前最轻量、最符合 Eloquent 语义的解法。它底层走的是左连接加 SUM() 聚合,一条 SQL 就能把活干完,根本不用担心 N+1 的问题。
但实际用起来,容易中招的地方主要有三个:
withSum('items')里这个'items'必须和模型里定义的关系方法名严格一致。比如你写的是public function items() { return $this->hasMany(OrderItem::class); },那这里就只能传'items',拼错了或者用了别的名字,结果就是空值。- 带条件查询时,闭包必须接收
$query参数,不能漏掉。正确写法是withSum(['items as total' => function ($query) { $query->where('status', 'active'); }])。闭包里的$query是个查询构建器实例,别在里边调select()或get(),那会直接把聚合逻辑搞崩,轻则报错,重则返回空值。 - 传闭包时,数组键和值的写法也有讲究。键是
'items as total',值是闭包。这样最终每个模型上就会多一个total属性。
看个具体例子:$orders = Order::withSum(['items as item_total' => fn($q) => $q->whereNotNull('paid_at')])->get();。跑完之后,每个 $order->item_total 就是该订单中那些已支付项的总金额。干净利落。
不过话说回来,withSum() 也不是万能的。如果遇到跨多层关联、需要加特别复杂的过滤条件,或者想一次性取多个不同条件的和——比如“今日点击数”和“历史总点击数”同时出现——那就得老老实实退回到查询构建器层面,手动写 LEFT JOIN 加 SUM()。
这时候,核心挑战在于字段别名和 GROUP BY 的匹配:
- 主表的字段必须显式列出来,不能图省事只写个
*。不然在 MySQL 5.7 及以上的严格模式下,GROUP BY会直接报错。 selectRaw()里SUM()的别名记得和后续 PHP 里访问的属性名保持一致。比如写了SUM(clicks.total) as today_clicks,后面就用$row->today_clicks取值。- 如果子查询里带了参数,别忘了调用
mergeBindings(),否则 where 条件不会生效,结果自然就不对了。
举个例子会更清楚:DB::table('affiliates')->leftJoin('referral_clicks as clicks', 'clicks.affiliate_id', '=', 'affiliates.id')->selectRaw('affiliates.*, SUM(clicks.clicks) as total_clicks')->groupBy('affiliates.id')->get();
这里有个容易混淆的点,Model::sum('field') 返回的是单个数字,适合用来统计全表或简单条件下的总和。而 withSum() 或 withCount() 是为每个模型实例注入一个新属性,返回的是一个模型集合。两者性质完全不同。
常见的坑有三个:
- 千万别写
Order::withSum('items')->sum('item_total')。因为withSum()后的数据还没 hydrate 成属性,sum()找不到item_total这个字段,结果要么返回 0,要么直接报错。 - 如果确实需要对预加载后的集合再求和,正确的做法是用集合方法:
$orders->sum('item_total')。但注意,这是在 PHP 层完成的,数据量大的时候性能可能会成为瓶颈。 - 别把
withCount()当withSum()用。虽然两者语法很像,但底层生成的 SQL 完全不同。withCount(['items as total'])只会返回一个计数,不可能给你金额总和。
最后聊一下子查询求和。虽然它在写法上很灵活——比如先算每个订单的 SUM(amount),再把结果跟主表 join 起来——但这玩意儿的代价也不小。MySQL 需要物化一个临时表,数据量一上来,性能直接跳水。Lara vel 官方文档也明确建议优先用 withSum() 或 join。
只有两种情况值得考虑用子查询:
- 聚合逻辑确实复杂到一定程度,涉及窗口函数或多次嵌套,用
JOIN根本写不出来。 - 你正在对数据进行分页,而主表和子表是一对多关系。直接用
JOIN会导致重复行,干扰count()的总数计算。这时候可以先在子查询里完成聚合,再跟主表分页查询。
来看一个结构示例(注意:不是推荐方案,只是说明逻辑):$sub = DB::table('order_items')->selectRaw('order_id, SUM(amount) as sum_amount')->groupBy('order_id'); DB::table('orders')->select('orders.*', 'sub.sum_amount')->joinSub($sub, 'sub', 'sub.order_id', '=', 'orders.id')->get();
其实,真正头疼的不是写 SQL 本身,而是聚合之后的数据一致性问题。比如订单状态变了,但缓存里的 item_total 还没刷新;或者并发修改子表时没有加事务锁。这些地方,比怎么写 SQL 更容易出岔子。


































