UNION ALL 后不能直接 GROUP BY,因集合操作优先级低于 GROUP BY,须用子查询或 CTE 包裹;需注意字段对齐、类型显式转换、条件下沉、去重语义等细节。

UNION ALL 之后不能直接 GROUP BY?先看执行顺序
SQL 中 UNION ALL 是集合操作,不是子句;它优先级低于 GROUP BY,所以写成 SELECT …… UNION ALL SELECT …… GROUP BY …… 会报错——语法不合法。真实执行流程是:先合并结果集,再对合并后的临时结果做聚合。必须用子查询或 CTE 包一层。
- 错误写法:
SELECT a FROM t1 UNION ALL SELECT a FROM t2 GROUP BY a→ 报错ERROR: syntax error at or near "GROUP" - 正确姿势:把
UNION ALL整体包进子查询,外层再GROUP BY - CTE 更清晰:
WITH src AS (SELECT …… UNION ALL SELECT ……) SELECT ……, COUNT(*) FROM src GROUP BY ……
多源头字段对齐:NULL 和类型不一致会静默出错
业务表字段名、顺序、类型常不统一,UNION ALL 要求列数相同、对应列类型兼容。MySQL 会尝试隐式转换(比如把字符串转成数字),PostgreSQL 则更严格,类型不匹配直接报错 UNION types text and integer cannot be matched。
- 显式转类型:
SELECT id::TEXT, amount FROM sales UNION ALL SELECT CAST(order_id AS TEXT), total FROM orders - 补缺失字段:
SELECT 'sales' AS source, id, amount, NULL::TEXT AS remark FROM sales UNION ALL SELECT 'orders', order_id, total, note FROM orders - 别依赖
NULL自动对齐:不同库对NULL类型推断不同,显式写NULL::NUMERIC或NULL::VARCHAR更稳
性能关键:加 WHERE 条件要塞进每个分支,别只在外层过滤
想统计“近 7 天订单 + 退款”,如果只在外层加 WHERE dt >= CURRENT_DATE - 7,数据库得先把所有历史数据 UNION ALL 完再过滤,IO 和内存爆炸。必须把时间条件下推到每个 SELECT 分支里。
- 慢:
WITH all_data AS (SELECT dt, amt FROM sales UNION ALL SELECT dt, amt FROM refunds) SELECT SUM(amt) FROM all_data WHERE dt >= '2024-06-01' - 快:
SELECT dt, amt FROM sales WHERE dt >= '2024-06-01' UNION ALL SELECT dt, amt FROM refunds WHERE dt >= '2024-06-01' - 索引是否生效,取决于每个分支的
WHERE是否能命中各自表的索引,别假设合并后还能用上
去重陷阱:UNION ALL ≠ UNION,但业务汇总常要“按主键去重”
UNION ALL 不去重,这是它快的原因。但业务中常遇到同一笔订单在销售表和发票表里都存在,直接 UNION ALL 后 COUNT(DISTINCT order_id) 可能掩盖重复计数问题。这时候得判断:到底要“记录数”还是“业务实体数”?
- 要实体数:别靠
UNION ALL,改用SELECT …… FROM sales FULL OUTER JOIN refunds USING (order_id)或分步UNION+ 去重逻辑 - 要记录数:确认各源头本身无重复(比如退款表有
status = 'success'过滤),否则先GROUP BY各自源头再UNION ALL - 别在
UNION ALL后加DISTINCT:它等价于UNION,失去ALL的性能优势,且可能误杀合法重复(如不同渠道同 ID 订单)
真正卡住的往往不是语法,而是没想清楚“这个 COUNT(*) 统计的是什么”。源头语义模糊时,强行堆 UNION ALL 只会让结果越来越不可信。