在 Laravel 中需用 DB::raw("DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00') as hour_group") 在 select() 中定义分组字段,并在 groupBy() 中重复相同 raw 表达式,确保 MySQL 严格模式下 SQL 正确执行。

在 Laravel 项目中需要对某张日志表按小时聚合统计,同时要求用 MySQL 的 DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00') 精确截断时间戳并分组,但 Query Builder 默认不支持该函数直接参与 groupBy —— 此时必须混合使用原生 SQL 片段与查询构建器链式调用,否则无法生成正确 SQL。
在 select() 中嵌入 DB::raw() 计算字段
第一步:用 DB::raw() 包裹日期格式化表达式,并显式声明别名。别名必须写出,否则 Laravel 不会将其识别为可选字段:
→ DB::raw("DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00') as hour_group")
第二步:将该 raw 表达式与其他普通字段一起传入 select(),注意不能混用字符串和 DB::raw() 实例——全部字段必须统一为数组元素形式:
→ ->select('service_name', DB::raw("DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00') as hour_group"), DB::raw('COUNT(*) as total'))
第三步:执行时若报错 “Expression #1 of SELECT list is not in GROUP BY clause”,说明 MySQL 严格模式开启,必须在 groupBy() 中重复写入该 raw 字段的原始表达式(不能只写别名):
→ ->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00')"))
【必须完全一致,包括引号类型和空格】
用 whereRaw() 替代复杂条件判断
方法一:当 WHERE 条件涉及数据库函数或运算符(如 JSON_CONTAINS、UNIX_TIMESTAMP 比较),且参数需绑定防注入时,用 whereRaw():
→ ->whereRaw('JSON_CONTAINS(metadata, ?)', [json_encode(['type' => 'error'])])
方法二:多条件组合且含括号优先级时,避免拆成多个 where() 导致逻辑错误:
→ ->whereRaw('(status = ? OR status = ?) AND created_at > DATE_SUB(NOW(), INTERVAL ? DAY)', ['failed', 'timeout', 7])
注意:whereRaw() 中的问号占位符顺序必须与参数数组严格对应,错一位会导致值被插到错误位置,查不到数据却无报错。
子查询作为 from 表的正确写法
第一步:先构建子查询实例,用 DB::table()->select()->where() 链式完成,不加 ->get():
→ $sub = DB::table('events')->select('user_id', DB::raw('MAX(created_at) as last_event'))->groupBy('user_id');
第二步:用 joinSub() 将其注册为临时表,第二个参数是别名,第三个参数是主表关联字段,第四个参数是子查询关联字段:
→ ->joinSub($sub, 'last_events', 'users.id', '=', 'last_events.user_id')
第三步:从主表 users 中 select 字段时,可直接引用子查询别名中的字段:
→ ->select('users.name', 'last_events.last_event')
【不能写成 'last_events.*',Laravel 8.75+ 会抛出 column not found 错误】
原生 SQL 与 Query Builder 分页共存
当原生 SQL 太复杂、无法安全转为 Query Builder 时,仍可用 paginate(),但必须手动提供总条数:
→ 先执行一条 COUNT(*) 原生 SQL 获取总数:$total = DB::select('SELECT COUNT(*) as cnt FROM (your complex sql) t')[0]->cnt;
→ 再用 DB::select() 执行带 LIMIT 和 OFFSET 的原生 SQL,OFFSET 值由当前页码和每页条数计算得出:
→ $offset = ($page - 1) * $perPage;
→ $sql = str_replace('SELECT *', "SELECT * LIMIT {$perPage} OFFSET {$offset}", $originalSql);
→ $data = DB::select($sql);
最后用 LengthAwarePaginator 手动封装结果:
→ new LengthAwarePaginator($data, $total, $perPage, $page, ['path' => request()->url(), 'query' => request()->query()]);


















