UNION 与集合操作:合并结果
UNION 基础
UNION 垂直合并两个或多个 SELECT 结果集。
基本语法:
SELECT columns FROM table1
UNION
SELECT columns FROM table2;示例:合并客户和供应商
-- 创建供应商表
CREATE TABLE suppliers (
id SERIAL PRIMARY KEY,
name VARCHAR(50),
email VARCHAR(100)
);
INSERT INTO suppliers (name, email) VALUES
('TechCorp', 'contact@techcorp.com'),
('Alice', 'alice@supplier.com'), -- 与 users 中的 Alice 重名
('Global Parts', 'sales@globalparts.com');
-- 合并用户和供应商的姓名
SELECT name FROM users
UNION
SELECT name FROM suppliers;结果:
name
--------------
Alice -- 去重:只出现一次
Bob
Charlie
Diana
Global Parts
TechCorp关键点:
- UNION 自动去重
- 结果按某种顺序排列(不保证)
UNION ALL:保留重复
UNION ALL 不去重,保留所有行。
SELECT name FROM users
UNION ALL
SELECT name FROM suppliers;结果:
name
--------------
Alice
Bob
Charlie
Diana
TechCorp
Alice -- 重复出现
Global PartsUNION vs UNION ALL:
- UNION:去重(需要排序和比较,较慢)
- UNION ALL:保留重复(更快)
推荐:如果确定没有重复或需要保留重复,使用 UNION ALL。
UNION 的规则
规则 1:列数必须相同
-- 错误:列数不匹配
SELECT name FROM users
UNION
SELECT name, email FROM suppliers; -- 2 列 vs 1 列错误信息:
ERROR: each UNION query must have the same number of columns规则 2:列类型必须兼容
-- 正确:VARCHAR 和 VARCHAR
SELECT name FROM users
UNION
SELECT name FROM suppliers;
-- 正确:INT 和 INT
SELECT id FROM users
UNION
SELECT id FROM suppliers;
-- 错误:VARCHAR 和 INT
SELECT name FROM users
UNION
SELECT id FROM suppliers;类型必须兼容或可隐式转换。
规则 3:列名来自第一个 SELECT
SELECT name AS user_name FROM users
UNION
SELECT name AS supplier_name FROM suppliers;结果列名是 user_name(第一个 SELECT 的列名)。
规则 4:ORDER BY 放在最后
SELECT name FROM users
UNION
SELECT name FROM suppliers
ORDER BY name; -- ORDER BY 放在最后,作用于整个结果不能在每个 SELECT 中单独排序(会被忽略):
-- 无效:ORDER BY 会被忽略
SELECT name FROM users ORDER BY name
UNION
SELECT name FROM suppliers ORDER BY name;实际应用场景
场景 1:合并不同来源的数据
-- 合并本地订单和在线订单
SELECT order_id, customer_name, 'Local' AS source
FROM local_orders
UNION ALL
SELECT order_id, customer_name, 'Online' AS source
FROM online_orders;场景 2:生成报表
-- 合并实际数据和汇总行
SELECT product, SUM(amount) AS total
FROM orders
GROUP BY product
UNION ALL
SELECT '总计', SUM(amount)
FROM orders
ORDER BY product;结果:
product | total
------------+---------
Headphones | 50.00
Keyboard | 80.00
Laptop | 1200.00
Monitor | 300.00
Mouse | 25.00
总计 | 1655.00场景 3:处理历史数据
-- 合并当前表和归档表
SELECT * FROM current_orders
WHERE order_date >= '2026-01-01'
UNION ALL
SELECT * FROM archived_orders
WHERE order_date >= '2026-01-01';INTERSECT:交集
INTERSECT 返回两个结果集的交集(同时存在于两个查询中的行)。
PostgreSQL 支持,MySQL 不支持。
-- 既是用户又是供应商的姓名
SELECT name FROM users
INTERSECT
SELECT name FROM suppliers;结果:
name
-------
AliceMySQL 替代方案(使用 INNER JOIN):
SELECT DISTINCT u.name
FROM users u
INNER JOIN suppliers s ON u.name = s.name;EXCEPT:差集
EXCEPT 返回第一个结果集中存在但第二个结果集中不存在的行。
PostgreSQL 支持,MySQL 不支持(MySQL 用 MINUS,但也不支持)。
-- 是用户但不是供应商的姓名
SELECT name FROM users
EXCEPT
SELECT name FROM suppliers;结果:
name
---------
Bob
Charlie
DianaMySQL 替代方案(使用 NOT IN 或 NOT EXISTS):
SELECT name FROM users
WHERE name NOT IN (SELECT name FROM suppliers);多个集合操作
可以组合多个 UNION、INTERSECT、EXCEPT:
-- 示例:A UNION B EXCEPT C
SELECT name FROM table_a
UNION
SELECT name FROM table_b
EXCEPT
SELECT name FROM table_c;优先级:INTERSECT > UNION/EXCEPT,从左到右执行。
使用括号明确优先级(部分数据库支持):
(SELECT name FROM table_a UNION SELECT name FROM table_b)
EXCEPT
SELECT name FROM table_c;UNION 与 JOIN 的区别
| 特性 | UNION | JOIN |
|---|---|---|
| 方向 | 垂直合并(增加行) | 水平合并(增加列) |
| 列数 | 必须相同 | 可以不同 |
| 去重 | UNION 去重 | 需要 DISTINCT |
| 用途 | 合并相似数据 | 组合关联数据 |
示例对比:
-- UNION:合并姓名(垂直)
SELECT name FROM users
UNION
SELECT name FROM suppliers;
-- 结果:1 列,多行
-- JOIN:组合用户和订单(水平)
SELECT u.name, o.product
FROM users u
INNER JOIN orders o ON u.id = o.user_id;
-- 结果:2 列,多行性能考虑
- UNION:需要去重(排序),较慢
- UNION ALL:不去重,更快
- 大结果集:优先使用 UNION ALL
- 索引:在 UNION 的源表列上创建索引可以提升性能
查看执行计划:
EXPLAIN SELECT name FROM users
UNION
SELECT name FROM suppliers;常见错误
错误 1:列数不匹配
-- 错误
SELECT name FROM users
UNION
SELECT name, email FROM suppliers;错误 2:ORDER BY 位置错误
-- 错误:ORDER BY 在 UNION 之前
SELECT name FROM users ORDER BY name
UNION
SELECT name FROM suppliers;正确:
SELECT name FROM users
UNION
SELECT name FROM suppliers
ORDER BY name;错误 3:混淆 UNION 和 JOIN
-- 错误意图:想要用户和订单的组合
SELECT name FROM users
UNION
SELECT product FROM orders;
-- 这只是合并姓名和产品名(无关联)
-- 正确:应该用 JOIN
SELECT u.name, o.product
FROM users u
INNER JOIN orders o ON u.id = o.user_id;练习
基本 UNION:合并 users 和 suppliers 的邮箱地址(去重)。
UNION ALL:合并 users 和 suppliers 的姓名(保留重复)。
添加来源标识:合并时添加一列标识数据来源('User' 或 'Supplier')。
带排序:合并 users 和 suppliers 的姓名,按字母顺序排序。
INTERSECT:查询既是用户又是供应商的邮箱地址(PostgreSQL)。
EXCEPT:查询是用户但不是供应商的姓名(PostgreSQL)。
MySQL 替代:用 JOIN 和 NOT IN 实现 INTERSECT 和 EXCEPT 的效果。
实际应用:创建一个销售报表,包含每个产品的销售额和一个总计行。
完成练习后,打开本章答案页核对。
本章小结
第三章学习了多表操作:
- INNER JOIN:组合匹配的行
- LEFT JOIN:保留左表所有行
- 子查询:嵌套查询逻辑
- EXISTS:高效检查存在性
- UNION:合并结果集
这些是关系数据库的核心能力。第四章将学习窗口函数和高级查询。