union结果不能直接跟group by,必须用子查询包裹后再聚合;需为子查询指定别名,优先用union all,列名和数据类型须对齐,过滤条件应下推至各select内部。

UNION结果不能直接跟GROUP BY,必须套一层子查询
直接在UNION语句末尾写GROUP BY会报错,比如SELECT a FROM t1 UNION SELECT a FROM t2 GROUP BY a——数据库会提示语法错误或“GROUP BY 无效位置”。因为UNION本身是集合操作,不是单个查询主体,GROUP BY只能作用于一个完整的SELECT结果集。
正确做法是把整个UNION包裹成子查询(也叫派生表),再对外层做聚合:
SELECT COUNT(*), status FROM ( SELECT status FROM orders_2024 UNION ALL SELECT status FROM orders_2025 ) AS combined GROUP BY status;
注意几点:
-
AS combined别名不可省略,所有主流数据库(MySQL、PostgreSQL、SQL Server)都强制要求子查询有别名 - 用
UNION ALL比UNION更稳妥:避免去重逻辑干扰计数,尤其当源表本身无重复但字段类型隐式转换导致“看似重复”时 - 如果原
UNION里用了ORDER BY或LIMIT,必须挪到子查询内部,外层不能再出现——否则语法非法
列名以第一个SELECT为准,聚合时别名要对齐
子查询里的列名完全继承自第一个SELECT子句。比如:
SELECT id, name FROM users UNION ALL SELECT user_id AS id, full_name AS name FROM admins
这段语句中,user_id AS id和full_name AS name的别名只是让语义清晰,真正生效的是第一个SELECT定义的id和name。如果第一个没写别名,后续即使写了也不会被采用。
聚合时若引用列,必须用第一个SELECT的列名(或其别名):
- ✅ 正确:
SELECT COUNT(*), name FROM ( ... ) AS t GROUP BY name - ❌ 错误:
SELECT COUNT(*), full_name FROM ( ... ) AS t GROUP BY full_name(full_name在结果集中不存在) - ⚠️ 隐患:如果第一个
SELECT用的是表达式(如UPPER(name)),那结果列名就是UPPER(name),聚合时得写GROUP BY UPPER(name),可读性差——建议统一加别名
聚合前需确认数据类型兼容,否则GROUP BY可能静默失败
两个SELECT中同序号列的数据类型不一致时,UNION会尝试隐式转换;但转换失败不会立刻报错,而是在GROUP BY阶段因值无法比较导致跳过某些行,或返回意外空结果。
典型例子:
- MySQL中
VARCHAR(10)和CHAR(20)union 后,GROUP BY可能因尾部空格处理不一致漏掉匹配 - PostgreSQL对
TEXT和CHAR严格区分,union会报错,必须显式CAST(col AS TEXT) - SQL Server中
NVARCHAR和VARCHAR混合,GROUP BY可能把相同内容判为不同值
安全做法:在每个SELECT子句中显式转成同一类型:
SELECT CAST(status AS VARCHAR(20)) AS status FROM orders_2024 UNION ALL SELECT CAST(state_flag AS VARCHAR(20)) AS status FROM legacy_orders
性能敏感场景下,UNION ALL + 外层聚合比UNION更可控
很多人以为UNION自动去重能简化逻辑,但在聚合统计中反而引入不确定性:
-
UNION去重发生在聚合之前,可能把本该分别计数的两条记录合并成一条,导致COUNT(*)偏小 -
UNION需要额外排序或哈希去重,IO和CPU开销明显高于UNION ALL - 当源表已通过业务逻辑保证无重复(如分表按时间切片),
UNION纯属冗余计算
除非你明确需要“去重后再统计”,否则默认用UNION ALL,把去重逻辑留在聚合层(比如用COUNT(DISTINCT user_id))更透明、更易调试。
最易被忽略的一点:子查询的WHERE条件如果放在外层,会先完成全部UNION再过滤,数据量大时极慢;应尽量把过滤下推到每个SELECT内部。










