
本文介绍在 Laravel 中为多个用户高效获取相同结构的聚合统计数据(如每日等级变化),解决使用 with() 预加载时因 GROUP BY 导致子查询为空的问题,并确保结果可排序、结构一致。
本文介绍在 laravel 中为多个用户高效获取相同结构的聚合统计数据(如每日等级变化),解决使用 `with()` 预加载时因 `group by` 导致子查询为空的问题,并确保结果可排序、结构一致。
在 Laravel 中,当需要为单个用户获取按天聚合的统计信息(例如最近 7 天的 MIN(level)、MAX(level) 和 SUM(level_change))时,直接链式调用 Eloquent 关系 + selectRaw() + groupBy() 即可正常工作。但一旦扩展到多个用户(例如通过 $room->users()->with('statistics') 预加载),若未正确约束预加载查询,Eloquent 会为每个用户执行一次无分组条件的统计查询,或因 GROUP BY 与关联逻辑冲突而返回空数组(如 "statistics": [])。
根本原因在于:with() 的闭包约束(constraining eager loads)必须显式指定所有字段(含 SELECT、WHERE、GROUP BY),且不能依赖模型默认的 id 或隐式主键关联逻辑;尤其当 statistics 表存在一对多关系时,GROUP BY DAY(date) 会破坏 Laravel 默认的关联绑定机制——因为分组后原始外键(如 user_id)未被包含在 SELECT 或 GROUP BY 中,导致 Eloquent 无法将聚合结果正确归属到对应用户。
✅ 正确做法是使用 带闭包的 with() 语法(Laravel 8+ 推荐写法),并在闭包中完整定义聚合逻辑,同时确保:
- 使用
selectRaw()明确选择date及聚合字段; -
WHERE条件限制时间范围(如>= now()->subDays(7)); -
GROUP BY必须包含能唯一标识时间粒度的表达式(如DAY(created_at)),但注意:DAY(date)不可用,因date是别名,应改用DAY(created_at)或更推荐DATE(created_at); - 若需后续按
sum_level_change排序,预加载本身不支持跨用户聚合结果排序,需在 PHP 层或通过联合查询(UNION)/子查询实现。
以下是修正后的完整示例:
Laravel Cloud(cloud.laravel.com)。此技能用于处理所有 Laravel Cloud 请求,包括搜索和读取数据。只要任务涉及 Laravel Cloud,即使用此技能。
return $room->users()
->with('statistics' => function ($query) {
$query->selectRaw('user_id, DATE(created_at) as date, MIN(level) as min_level, MAX(level) as max_level, SUM(level_change) as sum_level_change')
->whereDate('created_at', '>=', now()->subDays(7))
->groupByRaw('user_id, DATE(created_at)'); // ✅ 关键:按 user_id + 日期分组
})
->limit(25)
->get();
⚠️ 注意事项:
- 必须在
selectRaw()中显式包含user_id(外键),否则GROUP BY后 Eloquent 无法关联回用户; -
GROUP BY必须包含user_id和日期字段(推荐DATE(created_at)而非DAY(),避免跨月同日混淆); -
latest()在预加载中无效,排序需在主查询中处理(如orderByDesc('sum_level_change')不可行,因该字段在嵌套集合中);如需全局排序,建议获取数据后使用collect($users)->flatMap->statistics->sortByDesc('sum_level_change'); - 若统计表无索引,
WHERE DATE(created_at)可能导致全表扫描,建议添加组合索引:ALTER TABLE statistics ADD INDEX idx_user_date (user_id, created_at);
最终返回结构为每个用户附带其独立的 7 天聚合数组,语义清晰、性能可控,真正实现“同一逻辑复用于多用户”。










