union()自动去重,unionAll()保留重复项;后者性能更好、避免隐式转换错误,但要求子查询字段数严格一致、类型兼容,且排序分页等操作须在外层执行。

union() 和 unionAll() 的区别在哪
用 union() 合并两个 Eloquent 查询时,结果会自动去重;unionAll() 则保留所有行,包括重复项。实际中多数场景要的是“拼起来就完事”,比如把不同状态的订单查出来一起分页,这时候用 unionAll() 更快、更符合预期——它不走 DISTINCT 排序去重,数据库压力小,也避免因字段类型隐式转换导致的报错。
常见错误现象:SQLSTATE[HY000]: General error: 1222 The used SELECT statements have a different number of columns。这是因为两个 select() 字段数量或顺序不一致,不是字段名对不上,而是列数必须严格相等。
- 两个查询的
select()必须有相同数量的字段,且类型尽量兼容(比如都选id, name, created_at,别一个选id, name,另一个选id, name, status, created_at) - 字段别名要统一,否则合并后字段名以第一个查询为准,第二个查询同位置字段会被“覆盖”但不报错,容易取错数据
- 不能在每个子查询里单独用
orderBy()或limit()—— Laravel 会忽略,只认最外层的
怎么给 union 结果加 where / orderBy / paginate
union 查询本质是构造一个“虚拟表”,所以条件和排序必须挂在外层。直接在子查询上调用 where() 是无效的,Laravel 不会把它编译进最终 SQL 的外层。
正确做法:先用 DB::table() 或 Model::query() 构建子查询,再用 unionAll() 拼接,最后调用 where()、orderBy()、paginate()。
立即学习“PHP免费学习笔记(深入)”;
use Illuminate\Support\Facades\DB;
$first = Order::where('status', 'paid')->select('id', 'amount as value', DB::raw('"paid" as type'), 'created_at');
$second = Order::where('status', 'refunded')->select('id', 'amount as value', DB::raw('"refunded" as type'), 'created_at');
$combined = $first->unionAll($second)->orderBy('created_at', 'desc')->paginate(20);
注意:paginate() 会自动处理外层分页,但要求所有子查询的 select() 字段完全对齐。如果某个子查询漏了 created_at,分页时 orderBy 就会报错。
union 后怎么映射成模型实例
Eloquent 的 union 返回的是普通集合(Illuminate\Support\Collection),不是模型集合(Illuminate\Database\Eloquent\Collection),所以不能直接调用模型方法、访问访问器(accessor)、触发事件。
如果真需要模型行为,有两个务实选择:
- 不用
union,改用 PHP 层合并:分别查出两个模型集合,用merge()合并,再手动sortByDesc('created_at')和forPage()分页——适合数据量不大(单次 ≤ 500 条) - 坚持用数据库 union,但后续逻辑绕过模型特性:把结果当数组/StdClass 处理,字段值直接取,不依赖
getAttributes()或toArray()的修饰逻辑 - 强行转模型(不推荐):用
new static()手动 new 实例再 setAttribute,但关系、强制类型、日期转换全失效,容易埋坑
MySQL 8.0+ 的 WITH RECURSIVE 能替代 union 吗
不能。递归 CTE 解决的是树形结构(如分类层级、组织架构遍历),而 union 是横向拼接多个独立结果集。两者解决的问题维度完全不同。
有人试过用 CTE 模拟 union 行为,比如写两个 SELECT ... UNION ALL SELECT ... 套进 WITH t AS (...),这纯属绕路——既没提升可读性,又丧失了 Eloquent 对 union 的语法支持(比如 unionAll() 自动处理绑定参数),还可能触发 MySQL 对 CTE 的临时表限制。
真正要注意的是:union 查询无法使用索引下推(index pushdown),尤其当子查询本身就很慢时,合并后性能只会更差。建议先确保每个子查询都能走索引,再 union;如果子查询已带复杂 join 或 subquery,不如拆成两次请求 + PHP 合并。



















