laravel中应优先用查询构建器而非原生sql,因其支持链式调用、参数绑定与可维护性;子查询需用fromsub($query, 'alias')显式命名,窗口函数需配合selectraw()与数据库方言,动态条件推荐when()/tap()安全组装,复杂关联宜用join()替代with()以避免歧义和笛卡尔积。

为什么不能直接写原生 SQL 来优化复杂查询
在 Laravel 中,遇到多表关联、嵌套子查询、窗口函数或动态条件组合时,很多人第一反应是 DB::select() 写原生 SQL。但这样会失去 Query Builder 的链式构建能力、参数绑定安全性和后续的可维护性。更关键的是:Laravel 的查询构建器本身支持绝大多数复杂场景,只是需要理解它的表达边界和底层行为。
常见错误现象:QueryException: SQLSTATE[HY000]: General error: 1248 Every derived table must have its own alias —— 这往往是因为用 fromSub() 时漏了别名,而不是 Builder 不支持子查询。
- 子查询必须显式命名,用
fromSub($query, 'alias'),不能只传闭包 - 窗口函数需配合
raw()和数据库方言(如 MySQL 8.0+ 或 PostgreSQL),Builder 不自动加PARTITION BY语法 - UNION 查询要用
union()方法链,不能拼字符串;重复去重用union(),保留重复用unionAll()
如何用 when() 和 tap() 安全组装动态条件
复杂查询常伴随大量 if-else 判断字段是否存在、是否为空、是否启用某功能。硬写 if ($request->filled('status')) { $query->where('status', ...); } 会导致代码冗长且易漏掉 ->where() 的调用时机。Laravel 提供了两个轻量但关键的辅助方法。
使用场景:后台搜索页、API 过滤接口、导出数据前的条件预处理。
-
when()接收条件值和两个闭包:第一个在条件为真时执行,第二个(可选)在为假时执行;它返回的是当前查询实例,可继续链式调用 -
tap()更适合调试或副作用操作(如记录日志、触发监控),它不改变查询逻辑,只“路过”执行一次闭包 - 注意:不要在
when()闭包里 return 新查询对象,否则链会断开;应直接调用$query->where(...)等方法
$users = User::query()
->when($request->input('role'), function ($q, $role) {
$q->where('role_id', $role);
})
->when($request->filled('search'), function ($q, $search) {
$q->where(function ($sub) use ($search) {
$sub->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
});
})
->get();
什么时候该用 selectRaw() 而不是 select()
当需要数据库函数、类型转换、条件聚合(如 CASE WHEN)、JSON 字段提取(->>'$.name')或自定义别名含空格/特殊字符时,select() 无法满足。但滥用 selectRaw() 会引入 SQL 注入风险,且破坏 Builder 的列名映射逻辑。
性能影响:MySQL 中 selectRaw('COUNT(*) as total') 比 count() 方法少一次额外查询,但若同时要取数据又取总数,应考虑 withCount() 或分两步查,避免 SELECT COUNT(*) OVER() 这类窗口函数拖慢主查询。
- 永远对用户输入做参数绑定:
selectRaw('LOWER(?) as lower_name', [$name]),不要拼接字符串 - PostgreSQL 中 JSON 提取用
->>,MySQL 5.7+ 用JSON_UNQUOTE(JSON_EXTRACT(...))或简写->>'$.field' - 别名中含空格必须用反引号包裹:
selectRaw('COUNT(*) as `total count`')
关联预加载 + 延迟约束与复杂查询的冲突点
用 with(['posts' => fn ($q) => $q->where('published', true)]) 做约束预加载很常见,但它本质是生成独立的 LEFT JOIN 子句。一旦主查询本身已有 JOIN、GROUP BY 或 HAVING,就可能引发列歧义、笛卡尔积或 MySQL 的 Unknown column 错误。
容易踩的坑:withCount() 在 GROUP BY 场景下默认加 COUNT(*),但若你已用 selectRaw('COUNT(posts.id)'),再加 withCount('posts') 会导致重复聚合字段冲突。
- 优先用
join()显式控制关联逻辑,比with()更可控;尤其涉及多层嵌套或 ON 条件含函数时 - 需要过滤关联数据又不想丢失主表记录?用
leftJoin()+whereNull()或whereExists()替代whereHas() - GROUP BY 后统计,改用
selectRaw()配合聚合函数,避免混合 Eloquent 的withCount()和原生分组
复杂点始终在「JOIN 语义」和「聚合上下文」的切换上——Builder 不会自动帮你判断一个关联该走子查询还是 JOIN,得根据执行计划和数据分布手动权衡。











