组合查询
UNION / UNION ALL / 多表结果拼合 / 排序
前面讲的 JOIN 是把多张表的列横向拼接。但还有一种需求——把多个 SELECT 的结果纵向堆叠成一张表。
比如:
- 把"北京的员工"和"上海的员工"拼成一张清单
- 把"2024 上半年的订单"和"下半年的订单"合在一起
- 从两张结构相同的表(如
orders_2023、orders_2024)中取汇总数据
这就要用 UNION。
UNION:纵向拼接,自动去重
基本语法
把两条 SELECT 用 UNION 连起来:
sql
SELECT name, city, salary FROM users WHERE city = '北京'
UNION
SELECT name, city, salary FROM users WHERE city = '上海';+------+------+----------+
| name | city | salary |
+------+------+----------+
| 张三 | 北京 | 8000.00 |
| 孙七 | 北京 | 6000.00 |
| 郑十 | 北京 | 7800.00 |
| 李四 | 上海 | 12000.00 |
| 周八 | 上海 | 11000.00 |
+------+------+----------+北京 3 人 + 上海 2 人 = 5 行,纵向堆在一起。
UNION 的两条规则
- 每条 SELECT 的列数必须相同
- 对应列的数据类型必须兼容
sql
-- ❌ 列数不同,报错
SELECT name, city FROM users
UNION
SELECT name FROM users;
-- ❌ 列顺序不对应可能不出错但结果混乱
SELECT name, city FROM users -- 第1列name 第2列city
UNION
SELECT city, name FROM users; -- 第1列city 第2列name → 语义错乱📝 列名以第一条 SELECT 的为准,后面的列名会被忽略。
自动去重
UNION 默认会去掉重复行:
sql
SELECT city FROM users WHERE city IN ('北京', '上海')
UNION
SELECT city FROM users WHERE city IN ('上海', '广州');三条 SELECT 结果里有两次"上海",最终只出现一次:
+------+
| city |
+------+
| 北京 |
| 上海 |
| 广州 |
+------+UNION ALL:不去重,原样保留
如果你确定数据不会重复,或者就是要保留每一行,用 UNION ALL:
sql
SELECT city FROM users WHERE city IN ('北京', '上海')
UNION ALL
SELECT city FROM users WHERE city IN ('上海', '广州');+------+
| city |
+------+
| 北京 |
| 北京 |
| 北京 | ← 北京有 3 人
| 上海 |
| 上海 | ← 上海在两条 SELECT 里都出现了
| 上海 |
| 广州 |
+------+UNION vs UNION ALL 性能
| UNION | UNION ALL | |
|---|---|---|
| 去重 | ✅ 自动去重 | ❌ 不去重 |
| 性能 | 慢一点(要去重就得排序比较) | 更快 |
| 场景 | 结果可能有重复时 | 确认不重复,或故意要保留全部 |
💡 实用经验:绝大部分场景用
UNION ALL就够了——既然两条 SELECT 查的是不同条件,结果本来就大概率不重复。省掉去重的开销。
实战场景
场景 1:跨表合并
把两个结构相同的表合在一起查(比如按年份拆分的订单表):
sql
SELECT product, order_date FROM orders_2023
UNION ALL
SELECT product, order_date FROM orders_2024
ORDER BY order_date;场景 2:同表多条件(WHERE 写不下的情况)
查"工资最高的 3 个人"和"工资最低的 3 个人",拼成一份报告:
sql
(SELECT name, salary, '高薪' AS 标签 FROM users ORDER BY salary DESC LIMIT 3)
UNION ALL
(SELECT name, salary, '低薪' AS 标签 FROM users ORDER BY salary ASC LIMIT 3);+------+----------+------+
| name | salary | 标签 |
+------+----------+------+
| 赵六 | 15000.00 | 高薪 |
| 吴九 | 13000.00 | 高薪 |
| 李四 | 12000.00 | 高薪 |
| 孙七 | 6000.00 | 低薪 |
| 郑十 | 7800.00 | 低薪 |
| 张三 | 8000.00 | 低薪 |
+------+----------+------+注意:
- 括号不能省——每条 SELECT 里的
ORDER BY+LIMIT需要括号包起来 - 手动加了
'高薪' AS 标签让结果一眼能分辨来源
场景 3:生成汇总行
把明细和汇总拼在一张表里(报表常用):
sql
SELECT city AS 区域, COUNT(*) AS 人数
FROM users
GROUP BY city
UNION ALL
SELECT '合计' AS 区域, COUNT(*) AS 人数
FROM users;+------+------+
| 区域 | 人数 |
+------+------+
| 北京 | 3 |
| 上海 | 2 |
| 广州 | 1 |
| 深圳 | 1 |
| 杭州 | 1 |
| 合计 | 8 |
+------+------+最后一行手工加了"合计",非常直观。
UNION 上排序
UNION 的结果也能排序,ORDER BY 放在最后一条 SELECT 的后面:
sql
SELECT name, salary FROM users WHERE city = '北京'
UNION ALL
SELECT name, salary FROM users WHERE city = '上海'
ORDER BY salary DESC;+------+----------+
| name | salary |
+------+----------+
| 李四 | 12000.00 |
| 周八 | 11000.00 |
| 张三 | 8000.00 |
| 郑十 | 7800.00 |
| 孙七 | 6000.00 |
+------+----------+ORDER BY 对整个 UNION 的结果排序,不是只排最后一条。
⚠️ 不能单独给某条 SELECT 加 ORDER BY(除非用括号 + LIMIT 的组合),否则 MySQL 会报错或忽略。
UNION vs JOIN:一张表分清
| JOIN | UNION | |
|---|---|---|
| 方向 | 横向(加列) | 纵向(加行) |
| 关键语法 | ON 匹配条件 | 列数和类型一致 |
| 场景 | "把一个人和他的订单放在一行" | "把两个查询结果堆在一起" |
| 典型问法 | "张三买了什么?" | "北京人和上海人都有谁?" |
一张图:
JOIN:横着拼 UNION:竖着堆
users orders SELECT 1 结果
┌────────┐ ┌────────┐ ┌──────────┐
│ id name │ │ prod │ │ 张三 │
│ 1 张三 │ │ 鼠标 │ │ 李四 │
│ 2 李四 │ │ 键盘 │ └──────────┘
└────────┘ └────────┘ ┌──────────┐
+ + │ 赵六 │
= = │ 孙七 │
┌──────────────────┐ └──────────┘
│ 张三 鼠标 │ +
│ 张三 键盘 │ =
└──────────────────┘ ┌──────────┐
│ 张三 │
│ 李四 │
│ 赵六 │
│ 孙七 │
└──────────┘小结
UNION 是"纵向堆叠",和 JOIN 的"横向拼接"互补:
- UNION 去重 — 多个 SELECT 结果摞一起,重复行自动去掉
- UNION ALL 不去重 — 要保留所有行(包括重复)用它,性能也更好
- 列数要对齐 — 每个 SELECT 的列数、类型必须一致,不一致就报错
- 排序放最后 — ORDER BY 只能出现在最后一个 SELECT 后面,对所有结果生效
- UNION vs JOIN — UNION 是上下摞(加行),JOIN 是左右拼(加列),别搞混
自主练习
基于 users 表:
- 用 UNION 把"北京员工"和"工资 > 10000 的员工"拼在一起(注意去重)
- 用 UNION ALL 把"年龄小于 25 的人"和"年龄大于 30 的人"列出,加一列区分来源
- 按城市分组统计人数,最后加一行"总计"(UNION ALL 汇总行)
- 思考:UNION 和 JOIN 能组合使用吗?试着写出"北京客户的订单 + 上海客户的订单"(先 JOIN 再 UNION,或者先 UNION 再 JOIN)