先说结论
很多刚开始学习 SQL 的朋友,常常在 JOIN 和 UNION 之间感到困惑。虽然两者都涉及“合并数据表”的操作,但它们的合并方向完全不同,理解这一点是掌握 SQL 关联查询与数据整合的基础。

? JOIN 是水平方向合并,就像把两张 A4 纸左右并排粘贴在一起,形成一张更宽的纸张。
? UNION 是垂直方向合并,如同将两张纸上下拼接,得到一张更长的纸张。
先看一张示意图,一分钟就能理解它们之间的本质差异:
JOIN(水平合并): UNION(垂直合并):
表A 表B 结果 表A 结果
───── + ───── = ─────────── ───── ─────
行1 行X 行1 | 行X 行1 行1
行2 行Y 行2 | 行Y 行2 行2
行3 行Z 行3 | 行Z ───── = 行3
表B ─────
行A 行A
行B 行B
一、JOIN —— 横向拼接
核心思想
JOIN 的核心在于将同一业务实体的不同属性拼接在一起。两张表之间必须存在明确的关联关系,例如通过某个 ID 字段,将符合条件的行左右合并为一行,从而扩展数据维度。
典型场景
例如,我有一张订单表,想同时查看订单信息以及下单患者的姓名。这就是 JOIN 最典型的应用场景。
订单表和患者表通过 patient_id 字段建立关联,每条订单都能找到对应的患者信息。就像把一张发货单与客户资料关联起来,数据自然实现了横向扩展。
-- 查询订单,同时显示患者姓名
SELECT
o.order_no AS 订单号,
o.amount AS 金额,
u.real_name AS 患者姓名
FROM orders o
JOIN patient_user u ON o.patient_id = u.id
WHERE o.create_time >= '2024-01-01'
查询结果如下:
订单号 金额 患者姓名 ORDER_001 199.00 张三 ORDER_002 299.00 李四 ORDER_003 99.00 张三
每一行代表一条订单及其对应的患者信息,数据实现了横向扩展,列数增加,而行数基本保持不变(可能会因匹配关系略有调整)。
JOIN 的几种类型
-- INNER JOIN(内连接):仅返回两表中都能匹配成功的行 SELECT * FROM A INNER JOIN B ON A.id = B.a_id -- LEFT JOIN(左连接):左表全部返回,右表未匹配的行填充 NULL SELECT * FROM A LEFT JOIN B ON A.id = B.a_id -- RIGHT JOIN(右连接):右表全部返回,左表未匹配的行填充 NULL SELECT * FROM A RIGHT JOIN B ON A.id = B.a_id
? 记忆口诀:JOIN 的本质是“配对”,两行数据通过牵手合并为一行,牵手的条件由
ON后面的关联字段决定。
二、UNION —— 纵向堆叠
核心思想
UNION 用于处理结构相同但来源不同的数据集。它将多个查询结果垂直堆叠在一起,前提是各查询的列数和数据类型必须完全一致。
典型场景
比如,我有日常体重记录表,还有 InBody 专业测量表,想在趋势图中将两边的体重数据按时间顺序呈现在同一条折线上。这就是 UNION 的典型用法。
这两张表的记录之间没有一一对应关系——体重记录表可能一年有 365 条数据,InBody 表可能只有 12 条,它们都是独立的测量事件。用户关注的是所有体重数据按时间排列,而非哪条记录对应哪个 InBody 结果。因此,操作方式为 纵向堆叠:
-- 体重趋势图:将两张表的数据纵向合并 SELECT record_time, weight, '日常测量' AS source FROM patient_weight_record WHERE patient_id = 1 UNION ALL SELECT record_time, weight, 'InBody测量' AS source FROM patient_body_composition WHERE patient_id = 1 ORDER BY record_time
查询结果如下:
record_time weight source 2024-01-01 07:00 85.50 日常测量 2024-01-05 09:00 85.20 日常测量 2024-01-10 10:00 84.80 InBody测量 ← 来自另一张表 2024-01-15 07:30 84.50 日常测量 2024-01-20 08:00 84.10 日常测量 2024-02-10 10:00 83.60 InBody测量 ← 来自另一张表
数据实现了纵向扩展,列数不变,行数增加。
UNION 和 UNION ALL 的区别
-- UNION:自动去重(性能相对较差,需要额外排序和去重操作) SELECT name FROM table_a UNION SELECT name FROM table_b -- UNION ALL:不去重,直接合并(性能更优) SELECT name FROM table_a UNION ALL SELECT name FROM table_b
⚠️ 基本原则:如果确定两个结果集没有重复数据,或者不关心重复问题,优先选择 UNION ALL,性能更好。只有在明确需要去重时才使用 UNION。
三、核心对比
| 对比维度 | JOIN | UNION |
|---|---|---|
| ? 合并方向 | 水平(列数增加) | 垂直(行数增加) |
| ? 前提条件 | 两表需有关联字段 | 各查询列数和数据类型一致 |
| ? 结果形态 | 行数取决于匹配关系 | 行数 = 各查询结果行数之和 |
| ? 适用场景 | 同一实体的不同维度 | 同类数据的不同来源 |
| ? 关键字 | ON 指定关联条件 |
无需关联条件 |
| ⚡ 典型用法 | 订单 + 用户信息 | 多来源同类数据合并 |
四、如何判断用哪个?
遇到“将两张表合并”的需求时,可以问自己两个关键问题:
问题一:我期望的结果是列数增加,还是行数增加?
列数增加 → 选择 JOIN;行数增加 → 选择 UNION。
问题二:两张表的数据是“同一件事的不同角度”,还是“同类事情的不同来源”?
同一件事的不同角度(一条订单 + 该订单的用户名)
→ JOIN,水平拼接
同类事情的不同来源(体脂秤的体重记录 + InBody 的体重记录)
→ UNION,垂直堆叠
几个练习,检验你的理解
❓ 查询患者信息,同时显示主管医生的姓名 → 患者表 JOIN 医生表(同一患者档案的不同维度) ✅ JOIN ❓ 查询本月所有健康指标的预警记录(血压预警、血糖预警、体重预警分别存储在不同表) → 三张预警表垂直合并(同类事件的不同来源) ✅ UNION ALL ❓ 查询运动记录,同时展示当天的饮食热量 → 运动表 LEFT JOIN 饮食表(同一天的不同健康维度) ✅ JOIN ❓ 查询某患者的所有沟通记录(患者发送的、医生发送的、系统消息) → 这些数据实际在同一张表中,按 sender_type 区分,无需使用 UNION
五、常见误区
误区一:认为 JOIN 可以替代 UNION
-- ❌ 错误思路:用 JOIN 合并两张体重表 SELECT w.record_time, w.weight, b.weight FROM patient_weight_record w JOIN patient_body_composition b ON w.patient_id = b.patient_id -- 结果产生笛卡尔积:365条 × 12条 = 4380条,完全错误!
两张表没有行级别的对应关系,JOIN 会导致笛卡尔积,结果毫无意义。
误区二:UNION 的列未对齐
-- ❌ 错误:两个查询列数不一致 SELECT record_time, weight FROM patient_weight_record UNION ALL SELECT record_time, weight, bmi FROM patient_body_composition -- 报错:列数不匹配 -- ✅ 正确:列数和类型对齐,缺少的列用 NULL 补充 SELECT record_time, weight, NULL AS bmi FROM patient_weight_record UNION ALL SELECT record_time, weight, bmi FROM patient_body_composition
误区三:UNION 后遗漏 ORDER BY
-- ⚠️ 注意:ORDER BY 必须放在最后,对整体结果进行排序 SELECT record_time, weight FROM patient_weight_record WHERE patient_id = 1 UNION ALL SELECT record_time, weight FROM patient_body_composition WHERE patient_id = 1 ORDER BY record_time -- ✅ 放在最后,对合并后的全部结果排序
六、一句话总结
? JOIN 如同“配对”(两行数据牵手合并为一行,列数增加);UNION 如同“排队”(两批数据排成一列,行数增加)。想清楚你需要的是“更宽的表”还是“更长的表”,就能轻松做出正确选择。
