Laravel学生就餐查询优化:解决join/union合并慢查询报错
问题排查
原有代码性能差、结果不准的核心原因
- 参数传递错误:
Request::only('xxx')返回的是关联数组,不是单个参数值,直接传入where条件会导致SQL参数绑定异常,触发全表扫描,这是慢查询的核心诱因之一 - 未就餐查询用
whereDoesntHave生成NOT EXISTS子查询,如果表没加对应索引,子查询会逐行判断,数据量上来后性能骤降 - 已就餐查询逻辑有bug:只查了就餐记录,没有关联校验学生的在册状态,会把已经离校/未入学的学生的就餐记录也查出来,结果不符合业务要求
- 两次查询重复过滤在册学生条件,多了一次数据库交互,不必要的IO开销拉高了响应时间
合并查询报错原因
union()方法要求传入的是数据库查询构造器实例,你第一个$students查询提前调用了get()->map(),返回的是Eloquent集合,集合没有getBindings()方法,直接传参会抛出你看到的错误- 关联逻辑错误:第一个查询用inner join关联就餐表,本身就会过滤掉没有就餐记录的学生,根本查不出未就餐的人群
- 条件写错:第二个查询里
date_served >= serviceDate、meal_type_id >= mealType完全不符合业务要求,业务需要的是精确匹配指定日期、指定餐次 - 同样存在参数传递错误的问题,把
Request::only()返回的数组直接传入了where条件
优化方案
前置必做优化
先给两张表加联合索引,从数据库层面把查询效率拉满:
-- 学生表加站点、出入校时间的联合索引,快速过滤当日在册学生 ALTER TABLE students ADD INDEX idx_site_date (site_id, enter_date, exit_date); -- 就餐表加站点、学生ID、餐次、就餐日期的联合索引,快速匹配当日指定餐次的就餐记录 ALTER TABLE student_meals ADD INDEX idx_meal_match (site_id, student_id, meal_type_id, date_served);
所有查询参数直接用Request::input('参数名')获取单个值,不要用Request::only()传参给where条件。
最优实现(单次查询+内存分组)
不需要写union,也不需要跑两次查询,用一次左关联就能拉取全量符合条件的数据,再在内存里拆分已就餐/未就餐集合,性能是所有方案里最高的:
// 提前读取参数,避免重复调用 $serviceDate = request()->input('serviceDate'); $mealType = request()->input('mealType'); $search = request()->input('search'); $site = Auth::user()->site; // 单次查询拉取所有当日在册学生,左关联匹配当日指定餐次的就餐记录 $allStudents = $site->students() ->leftJoin('student_meals as sm', function ($join) use ($serviceDate, $mealType) { $join->on('sm.student_id', '=', 'students.id') ->where('sm.meal_type_id', $mealType) ->where('sm.date_served', $serviceDate); }) ->where('students.enter_date', '<=', $serviceDate) ->where('students.exit_date', '>=', $serviceDate) ->when($search, function ($q) use ($search) { // 如果你实际用first_name存姓名,把下面的name换成first_name即可 $q->where('students.name', 'like', "%{$search}%"); }) ->orderBy('students.name') ->select([ 'students.id', 'students.name', 'sm.id as meal_id', 'sm.created_at as meal_at' ]) ->get(); // 内存拆分两个结果集,无额外IO开销 $unservedStudents = $allStudents->whereNull('meal_id') ->map(fn ($s) => ['id' => $s->id, 'name' => $s->name]) ->values(); $servedStudents = $allStudents->whereNotNull('meal_id') ->sortByDesc('meal_at') ->map(fn ($s) => [ 'id' => $s->meal_id, 'student' => ['id' => $s->id, 'name' => $s->name] ]) ->values(); // 返回最终结果 return [ 'unservedStudents' => $unservedStudents, 'servedStudents' => $servedStudents ];
方案说明
- 整个逻辑只和数据库交互一次,避免了重复查询和子查询的开销,千条数据级别响应时间可以控制在10ms以内
- 左关联的特性会保留所有当日在册的学生,匹配到就餐记录的会带上就餐ID,没匹配到的就餐ID为null,刚好对应已就餐/未就餐两类人群
- 修正了原有代码里已就餐学生不校验在册状态的bug,返回结果完全符合业务要求
- 内存拆分集合的操作对于千条级别的数据来说耗时不到1ms,完全可以忽略
- 所有查询字段加了表前缀,避免连表后出现字段歧义报错
内容的提问来源于stack exchange,提问作者user19106040
相关产品推荐
相关产品推荐

