如何用Laravel子查询实现关联数据求和?
- 内容介绍
- 文章标签
- 相关推荐
本文共计996个文字,预计阅读时间需要4分钟。
如果只想查询主模型的某个字段的总和,同时希望获得最轻量级、最直观的选择,可以使用以下方法:
常见错误是以为它支持嵌套关联或复杂条件——其实不支持。比如 User::withSum('posts.comments', 'votes') 会报错,Laravel 不允许跨两级关联直接求和。
- 只支持一级关联:如
posts、orders,不能是posts.comments - 第二个参数必须是字段名字符串,不能是表达式(如
'price * quantity') - 结果以
{relation}_sum_{column}形式注入模型属性,例如$user->posts_sum_views
示例:
User::withSum('orders', 'total')->get();生成的 SQL 类似:SELECT *, SUM(orders.total) AS orders_sum_total FROM users LEFT JOIN orders ON users.id = orders.user_id GROUP BY users.id
手动写子查询用 addSelect() + selectRaw()
需要跨多级关联、加 WHERE 条件、或对表达式求和时,就得自己构造子查询。核心是用 addSelect() 把子查询结果作为额外字段塞进主查询,避免 N+1 或多次查询。
容易踩的坑是子查询里没正确关联外层表,导致结果全为 NULL 或重复计数。Laravel 的子查询默认不自动绑定外部变量,得用 use ($userId) 显式传递,或用 whereColumn() 做列对列关联。
- 子查询中优先用
whereColumn('users.id', 'orders.user_id')而不是where('orders.user_id', $user->id),否则无法复用到集合查询 - 记得给子查询加
as别名,否则 ORM 解析失败 - 聚合函数必须配
groupBy(除非子查询只查单条),但主查询不需要
示例(查每个用户近30天订单金额总和):
User::addSelect([ 'recent_orders_sum' => Order::selectRaw('SUM(total)') ->whereColumn('orders.user_id', 'users.id') ->where('orders.created_at', '>=', now()->subDays(30)) ->getQuery() ])->get();
HasOneThrough / HasManyThrough 不适合求和场景
看到“跨表关联”,有人第一反应是定义 HasManyThrough 关系,比如 User → Order → OrderItem。但这只是为了方便调用 $user->orderItems,跟求和完全无关——它本身不提供聚合能力,也不能被 withSum() 识别。
真正的问题在于:这类关系底层是两次 JOIN,而求和需要的是子查询或 GROUP BY,二者执行计划不同。强行用 withSum('orderItems', 'price') 会因笛卡尔积导致金额翻倍。
- 不要为求和专门定义 Through 关系
- 如果已有 Through 关系且必须复用,求和逻辑必须单独写子查询,不能依赖关系方法
- Through 关系适合“取数据”,不适合“算总数”
子查询求和的性能关键点
子查询本身不慢,慢在没索引、JOIN 太宽、或子查询被重复执行。最常被忽略的是外键缺失索引——比如 orders.user_id 没建索引,子查询就会全表扫描。
- 确保所有
whereColumn()涉及的外键都有索引 - 避免在子查询里用
LIKE '%xxx'或函数包裹字段(如DATE(created_at)),否则索引失效 - 用
EXPLAIN看执行计划,重点确认子查询是否走了索引、是否用了临时表或文件排序 - 如果子查询逻辑固定且数据量大,考虑用数据库视图或物化汇总表替代实时计算
关联求和看着简单,实际卡点往往不在写法,而在索引和数据分布。先跑 EXPLAIN,再改代码。
本文共计996个文字,预计阅读时间需要4分钟。
如果只想查询主模型的某个字段的总和,同时希望获得最轻量级、最直观的选择,可以使用以下方法:
常见错误是以为它支持嵌套关联或复杂条件——其实不支持。比如 User::withSum('posts.comments', 'votes') 会报错,Laravel 不允许跨两级关联直接求和。
- 只支持一级关联:如
posts、orders,不能是posts.comments - 第二个参数必须是字段名字符串,不能是表达式(如
'price * quantity') - 结果以
{relation}_sum_{column}形式注入模型属性,例如$user->posts_sum_views
示例:
User::withSum('orders', 'total')->get();生成的 SQL 类似:SELECT *, SUM(orders.total) AS orders_sum_total FROM users LEFT JOIN orders ON users.id = orders.user_id GROUP BY users.id
手动写子查询用 addSelect() + selectRaw()
需要跨多级关联、加 WHERE 条件、或对表达式求和时,就得自己构造子查询。核心是用 addSelect() 把子查询结果作为额外字段塞进主查询,避免 N+1 或多次查询。
容易踩的坑是子查询里没正确关联外层表,导致结果全为 NULL 或重复计数。Laravel 的子查询默认不自动绑定外部变量,得用 use ($userId) 显式传递,或用 whereColumn() 做列对列关联。
- 子查询中优先用
whereColumn('users.id', 'orders.user_id')而不是where('orders.user_id', $user->id),否则无法复用到集合查询 - 记得给子查询加
as别名,否则 ORM 解析失败 - 聚合函数必须配
groupBy(除非子查询只查单条),但主查询不需要
示例(查每个用户近30天订单金额总和):
User::addSelect([ 'recent_orders_sum' => Order::selectRaw('SUM(total)') ->whereColumn('orders.user_id', 'users.id') ->where('orders.created_at', '>=', now()->subDays(30)) ->getQuery() ])->get();
HasOneThrough / HasManyThrough 不适合求和场景
看到“跨表关联”,有人第一反应是定义 HasManyThrough 关系,比如 User → Order → OrderItem。但这只是为了方便调用 $user->orderItems,跟求和完全无关——它本身不提供聚合能力,也不能被 withSum() 识别。
真正的问题在于:这类关系底层是两次 JOIN,而求和需要的是子查询或 GROUP BY,二者执行计划不同。强行用 withSum('orderItems', 'price') 会因笛卡尔积导致金额翻倍。
- 不要为求和专门定义 Through 关系
- 如果已有 Through 关系且必须复用,求和逻辑必须单独写子查询,不能依赖关系方法
- Through 关系适合“取数据”,不适合“算总数”
子查询求和的性能关键点
子查询本身不慢,慢在没索引、JOIN 太宽、或子查询被重复执行。最常被忽略的是外键缺失索引——比如 orders.user_id 没建索引,子查询就会全表扫描。
- 确保所有
whereColumn()涉及的外键都有索引 - 避免在子查询里用
LIKE '%xxx'或函数包裹字段(如DATE(created_at)),否则索引失效 - 用
EXPLAIN看执行计划,重点确认子查询是否走了索引、是否用了临时表或文件排序 - 如果子查询逻辑固定且数据量大,考虑用数据库视图或物化汇总表替代实时计算
关联求和看着简单,实际卡点往往不在写法,而在索引和数据分布。先跑 EXPLAIN,再改代码。

