laravel模型聚合是数据库统计的快捷封装而非sql替代品;sum()等返回null而非0,需用??0处理;groupby需严格匹配字段;withcount/withsum闭包仅限关联表;大表count(*)应避免裸用。

直接说结论:Laravel 模型聚合不是“写 SQL 的替代品”,而是数据库层统计的快捷封装;用错方法会返回 null 而不是 0,导致 PHP 运行时错误。
sum()、avg()、max()、min() 返回 null,不是 0
MySQL 对空结果集的 SUM()、AVG() 默认返回 NULL,而 COUNT() 返回 0。Laravel 的 sum()、avg() 等方法底层直通 SQL,不自动转 0。
- 错误写法:
$total = Order::where('status', 'paid')->sum('amount') * 1.1—— 若无匹配记录,sum()返回null,PHP 报Trying to perform arithmetic on a null value - 正确写法:
$total = Order::where('status', 'paid')->sum('amount') ?? 0 - 若必须在 SQL 层补 0,用
DB::raw('COALESCE(SUM(amount), 0)'),但注意这会让结果变成数组,丢失模型实例
groupBy() 在 MySQL 8.0+ 严格模式下容易报错
只写 groupBy('user_id') 却在 select() 里包含非分组字段(如 name 或 created_at),MySQL 8.0+ 会直接抛出 Expression #1 of SELECT list is not in GROUP BY clause。
- 安全写法一:所有
select()字段都出现在groupBy()中,例如select('user_id', 'name')->groupBy('user_id', 'name') - 安全写法二:只选分组字段 + 聚合字段,例如
select('user_id')->selectRaw('COUNT(*) as order_count')->groupBy('user_id') - 别依赖
select('*')->groupBy('user_id')—— 它在旧版 MySQL 可能“凑合过”,但语义错误、结果不可靠
withCount() 和 withSum() 的闭包条件只对关联表生效
withCount() 和 withSum() 的闭包参数(如 function ($q) { $q->where('status', 'paid'); })只能过滤关联表数据,不能引用主模型字段,也不能影响主模型筛选逻辑。
- 有效:
withSum(['orders' => function ($q) { $q->where('status', 'paid'); }], 'amount')→ 正确限制 orders 表 - 无效:
withSum(['orders' => function ($q) { $q->where('users.active', 1); }], 'amount')→users是主表,闭包里查不到 - 软删除注意:闭包里加
withoutTrashed()无效;应确保关联关系定义中已处理(如->withTrashed()或默认排除)
大表 count(*) 不要裸用,尤其带复杂 where 条件时
全表扫描式 count(*) 在百万级表上可能秒变慢查询,且无法利用索引优化。
- 替代方案一:用
simplePaginate()替代paginate(),它不执行count(*),适合“下一页”场景 - 替代方案二:对近似值可接受的场景,用
DB::table('orders')->value('table_rows')(需权限)或缓存预估数 - 真要精确总数且性能敏感?建覆盖索引,比如
WHERE status = ?就给status加索引
真正难的不是调哪个方法,而是判断该不该在数据库层聚合——比如“每个用户最近一笔订单金额”,withCount() 和 withSum() 都无能为力,得用子查询或 selectRaw 配合窗口函数。











