union自动去重并按第一列升序排序,union all仅合并结果集、无去重和排序;实际多数场景应优先用union all,因其更快、更可控,尤其当业务已确保无重复或需自定义去重逻辑时。

Union 查询本身不自动去重,UNION ALL 更是完全不去重;想合并后去重,必须用 UNION,或在 PHP 层/子查询里手动处理。
UNION 和 UNION ALL 的行为差异直接影响去重结果
很多人误以为 unionAll() 只是“快一点的 union”,其实它连去重逻辑都不参与。MySQL 中:UNION 会隐式做 DISTINCT(对整行去重),而 UNION ALL 就是原样拼接,哪怕两段结果一模一样也会出现两次。
-
UNION要求所有子查询字段数、类型兼容,且会额外排序(MySQL 默认按第一列升序),可能拖慢大结果集 -
UNION ALL不校验重复、不排序,性能高,但结果里重复项得靠你后续加distinct(true)或group()处理 - ThinkPHP 的
union()方法默认生成UNION,unionAll()方法才生成UNION ALL;别混用或漏写方法名
ThinkPHP 中用 union() 做跨表去重的正确写法
直接写 union() 并不能保证最终结果“按某字段去重”,它只对完整行去重 —— 所以前提是各子查询的 field() 完全一致,否则数据库会报错或结果不可控。
- 必须显式调用
field(),且所有union()子查询字段顺序、别名、数量要严格一致 - 错误示例:
Db::name('a')->field('id,name')->union('SELECT id FROM b')->select()→ 字段数不匹配,MySQL 报错 - 正确示例:
Db::name('user_0')->field('id, name, status')->union('SELECT id, name, status FROM user_1')->select() - 如果原始表结构不同,得用 AS 统一别名,比如
SELECT uid AS id, uname AS name, 0 AS status FROM old_user
union 后还要按单字段取最新一条?别硬套 distinct()
常见需求:“从 user_0 和 user_1 两个分表中,每个 user_id 只取 create_time 最大的那条”。这时仅靠 union() + distinct(true) 没用 —— distinct() 只能去整行,无法控制“留哪一条”。
- 先
union合并,再用子查询 +GROUP BY:构造临时表,SELECT * FROM (union 结果) t GROUP BY user_id ORDER BY max(create_time) DESC - ThinkPHP 中推荐闭包写法:
$sub = Db::name('user_0')->field('user_id, max(create_time) as latest')->group('user_id')->union(...);,再用join关联回详情 - 注意 MySQL 5.7+ 的
ONLY_FULL_GROUP_BY模式:SELECT *配GROUP BY user_id会报错,必须把非分组字段用聚合函数包裹,或明确列出
分页时 union 结果去重容易翻车
ThinkPHP 对 union 查询的分页支持有限:paginate() 是在外层加 LIMIT,但去重发生在 UNION 内部,导致“第一页显示 10 条,实际去重后只剩 3 条”这种现象。
- 不要直接对
union链式调用paginate(),尤其当数据量大、重复率高时 - 稳妥做法:先执行
union得到完整 ID 列表(去重后),再用where in分页查详情 - 或者用原生 SQL 手动写
WITH RECURSIVE或临时表(TP6.1+ 支持with()),避开框架分页封装的盲区
union 去重不是黑盒操作,它依赖字段对齐、数据库行为、甚至 MySQL 版本配置。最易被忽略的是:你以为的“去重”其实是“按行比对”,而业务要的往往是“按业务主键聚合取优”,这两者之间差了一个子查询的距离。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











