Laravel8+MySQL8环境下如何在WHERE条件中使用HAVING子句
问题原因
你当前的代码和期望SQL都存在基础语法错误:
- SQL标准中
HAVING子句不能嵌套在WHERE条件的分组内部。SQL执行顺序为FROM -> WHERE -> GROUP BY -> SELECT -> HAVING -> ORDER BY,WHERE阶段还没计算SELECT里的自定义列,HAVING是在SELECT计算完成后执行的筛选段,不能插到WHERE的条件块里。 - Laravel查询构造器中,在
where()闭包内调用having()/orHaving()属于错误用法,构造器会直接把HAVING关键字拼接到WHERE语句段中间,生成数据库无法解析的非法SQL。
可行实现方案
你的运行环境是MySQL 8 + Laravel 8,以下两种方案都可以实现对自定义计算列的筛选:
方案1:HAVING分组筛选(写法最简洁)
MySQL本身做了语法扩展:没有GROUP BY时,HAVING子句支持直接引用SELECT中定义的列别名,可直接用于行级数据筛选。你只需要把所有针对自定义列的筛选逻辑,统一放到HAVING的分组闭包中即可,逻辑分组的写法和WHERE完全一致。
首先确保你的两个自定义列是用原始表达式定义的,避免被框架转义:
// 定义计算列 $booking_start_at = DB::raw("CAST(CONCAT(booking_start_date, 'T', booking_start_time) AS DATETIME) AS booking_start_at"); $booking_end_at = DB::raw("CAST(CONCAT(booking_end_date, 'T', booking_end_time) AS DATETIME) AS booking_end_at");
查询代码如下:
Booking::select('*', $booking_start_at, $booking_end_at) ->where('room_name', $data['room_name']) // 自定义列的筛选全部放到having分组中 ->having(function ($query) use ($data) { $rangeStart = Carbon::parse($data['booking_start_date'] . $data['booking_start_time']); $rangeEnd = Carbon::parse($data['booking_end_date'] . $data['booking_end_time']); $query->where(function ($q) use ($rangeStart) { $q->having('booking_start_at', '>', $rangeStart) ->orHaving('booking_end_at', '<', $rangeStart); }) ->where(function ($q) use ($rangeEnd) { $q->having('booking_start_at', '>', $rangeEnd) ->orHaving('booking_end_at', '<', $rangeEnd); }); }) ->get();
该代码生成的SQL符合语法规范,可直接正常执行:
SELECT *, CAST(CONCAT(booking_start_date, 'T', booking_start_time) AS DATETIME) AS booking_start_at, CAST(CONCAT(booking_end_date, 'T', booking_end_time) AS DATETIME) AS booking_end_at FROM `bookings` WHERE `room_name` = ? HAVING ( (`booking_start_at` > ? OR `booking_end_at` < ?) AND (`booking_start_at` > ? OR `booking_end_at` < ?) )
方案2:子查询包裹(兼容性最好)
如果需要兼容不支持HAVING直接筛选行数据的数据库场景,可以把计算列逻辑放到子查询中,外层查询直接用WHERE筛选别名,完全不需要用到HAVING:
// 构造包含计算列的子查询 $subQuery = Booking::select('*', DB::raw("CAST(CONCAT(booking_start_date, 'T', booking_start_time) AS DATETIME) AS booking_start_at"), DB::raw("CAST(CONCAT(booking_end_date, 'T', booking_end_time) AS DATETIME) AS booking_end_at") ); // 外层查询直接用where筛选自定义列 $results = DB::table(DB::raw("({$subQuery->toSql()}) AS booking_sub")) ->mergeBindings($subQuery->getQuery()) // 合并子查询的参数绑定 ->where('room_name', $data['room_name']) ->where(function ($query) use ($data) { $rangeStart = Carbon::parse($data['booking_start_date'] . $data['booking_start_time']); $rangeEnd = Carbon::parse($data['booking_end_date'] . $data['booking_end_time']); $query->where(function ($q) use ($rangeStart) { $q->where('booking_start_at', '>', $rangeStart) ->orWhere('booking_end_at', '<', $rangeStart); }) ->where(function ($q) use ($rangeEnd) { $q->where('booking_start_at', '>', $rangeEnd) ->orWhere('booking_end_at', '<', $rangeEnd); }); }) ->get();
避坑提示
- 不要在
where()闭包内调用having系列方法,Laravel不会自动把这类条件移动到HAVING语句段,只会直接拼接生成错误SQL - 传入Carbon实例作为查询参数时,Laravel会自动转换为MySQL兼容的datetime格式字符串,不需要手动调用
->toDateTimeString() - 你之前写的期望SQL本身不符合SQL语法,HAVING关键字不能出现在WHERE的条件括号组内,任何数据库都无法解析这类语句
内容的提问来源于stack exchange,提问作者Mudit Gulgulia
相关产品推荐
相关产品推荐

