联结表
INNER JOIN / LEFT JOIN / RIGHT JOIN / 表别名 / 多表查询
为什么数据要分多张表?
到目前为止,我们一直在和一张 users 表打交道。但现实中,一个公司的数据绝不会只塞在一张表里。
比如你现在要记录每个员工的订单:
- 张三下了 3 单
- 李四下了 1 单
- 王五下了 5 单
如果全放 users 表里,会变成这样:
users 表(错误做法)
+----+------+------------------+
| id | name | orders |
+----+------+------------------+
| 1 | 张三 | 鼠标,键盘,显示器 | ← 一个字段塞三个值,怎么查?
+----+------+------------------+这有多痛苦:
- 想查"张三买了鼠标没"?得用
LIKE '%鼠标%'去字符串里扒 - 想统计"键盘卖了多少个"?得把所有行的 orders 字段拆开再数
- 想加个"下单时间"?一个字段里塞不下了
正确的做法:分表。一张 users 存人,一张 orders 存订单:
users 表 orders 表
+----+------+------+ +----+---------+----------+------------+
| id | name | city | | id | user_id | product | order_date |
+----+------+------+ +----+---------+----------+------------+
| 1 | 张三 | 北京 | | 1 | 1 | 鼠标 | 2024-01-05 |
| 2 | 李四 | 上海 | | 2 | 1 | 键盘 | 2024-01-05 |
| 3 | 王五 | 广州 | | 3 | 1 | 显示器 | 2024-02-10 |
| 4 | 赵六 | 深圳 | | 4 | 2 | 鼠标 | 2024-03-15 |
| 5 | 孙七 | 北京 | | 5 | 3 | 键盘 | 2024-01-20 |
| 6 | 周八 | 上海 | | 6 | 3 | 耳机 | 2024-02-18 |
| 7 | 吴九 | 杭州 | | 7 | 3 | 鼠标 | 2024-04-01 |
| 8 | 郑十 | 北京 | | 8 | 3 | 数据线 | 2024-04-22 |
| | | | | 9 | 3 | U盘 | 2024-05-10 |
| | | | | 10 | 5 | 耳机 | 2024-06-01 |
+----+------+------+ +----+---------+----------+------------+orders.user_id 指向 users.id,这叫外键。分表后数据干净、查询灵活、不会出现"一个字段塞多个值"的尴尬。
准备 orders 表
在你的 tutorial 库里直接跑:
-- 建表
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
product VARCHAR(50) NOT NULL,
order_date DATE NOT NULL
);
-- 塞数据
INSERT INTO orders (user_id, product, order_date) VALUES
(1, '鼠标', '2024-01-05'),
(1, '键盘', '2024-01-05'),
(1, '显示器', '2024-02-10'),
(2, '鼠标', '2024-03-15'),
(3, '键盘', '2024-01-20'),
(3, '耳机', '2024-02-18'),
(3, '鼠标', '2024-04-01'),
(3, '数据线', '2024-04-22'),
(3, 'U盘', '2024-05-10'),
(5, '耳机', '2024-06-01');user_id 对应 users 表的 id。跑完 SELECT * FROM orders; 看到 10 行就妥了。
但问题来了——数据分开了,怎么合在一起查?比如"列出所有订单,同时显示下单人的名字"?
这就要用到 联结(JOIN)。
INNER JOIN:取两表的交集
INNER JOIN 是最常用的联结——只返回两表中能匹配上的行。
基本语法
把订单和用户名合在一起查:
SELECT users.name, orders.product, orders.order_date
FROM users
INNER JOIN orders ON users.id = orders.user_id;+------+---------+------------+
| name | product | order_date |
+------+---------+------------+
| 张三 | 鼠标 | 2024-01-05 |
| 张三 | 键盘 | 2024-01-05 |
| 张三 | 显示器 | 2024-02-10 |
| 李四 | 鼠标 | 2024-03-15 |
| 王五 | 键盘 | 2024-01-20 |
| 王五 | 耳机 | 2024-02-18 |
| 王五 | 鼠标 | 2024-04-01 |
| 王五 | 数据线 | 2024-04-22 |
| 王五 | U盘 | 2024-05-10 |
| 孙七 | 耳机 | 2024-06-01 |
+------+---------+------------+一行一行看这条 SQL:
FROM users— 从 users 表开始INNER JOIN orders— 把 orders 表"联结"进来ON users.id = orders.user_id— 联结条件:users 的 id 等于 orders 的 user_idSELECT users.name, orders.product— 从两张表里挑需要的列
📝
ON是联结的"匹配规则"。没有 ON 的话 MySQL 会把 users 的每一行和 orders 的每一行强行配对,产生笛卡尔积——8 人 × 10 单 = 80 行,大部分都是无意义的。
谁被"漏掉"了?
注意看结果——赵六、周八、吴九、郑十没有出现。
因为 INNER JOIN 只返回两张表都匹配得上的行。赵六(id=4)虽然在 users 表里存在,但 orders 表里没有 user_id=4 的行,所以在 INNER JOIN 的结果中消失了。
INNER JOIN = 两表的交集
users ∩ orders
users orders
┌──────┐ ┌──────┐
│ 赵六 │ │ 张三 │
│ 周八 │ │ 李四 │
│ 吴九 │ │ 王五 │
│ 郑十 │ │ 孙七 │
└──────┘ └──────┘
\ /
交集:张三、李四、王五、孙七如果想把没下过单的人也列出来,得用 LEFT JOIN。
LEFT JOIN:左表全保留
LEFT JOIN 保留左表(FROM 后面的表)的每一行,右表匹配不上就填 NULL:
SELECT users.name, orders.product, orders.order_date
FROM users
LEFT JOIN orders ON users.id = orders.user_id;+------+---------+------------+
| name | product | order_date |
+------+---------+------------+
| 张三 | 鼠标 | 2024-01-05 |
| 张三 | 键盘 | 2024-01-05 |
| 张三 | 显示器 | 2024-02-10 |
| 李四 | 鼠标 | 2024-03-15 |
| 王五 | 键盘 | 2024-01-20 |
| 王五 | 耳机 | 2024-02-18 |
| 王五 | 鼠标 | 2024-04-01 |
| 王五 | 数据线 | 2024-04-22 |
| 王五 | U盘 | 2024-05-10 |
| 赵六 | NULL | NULL | ← 没订单,留空
| 孙七 | 耳机 | 2024-06-01 |
| 周八 | NULL | NULL | ← 没订单,留空
| 吴九 | NULL | NULL | ← 没订单,留空
| 郑十 | NULL | NULL | ← 没订单,留空
+------+---------+------------+8 个人全在,但有 4 个人 product 和 order_date 是 NULL——说明他们没下过单。
💡 一句话记:LEFT JOIN = 左表全都要,右表有的接上,没有就 NULL。
实战:找没下过单的人
利用 LEFT JOIN + IS NULL 精准捞出"沉默用户":
SELECT users.name
FROM users
LEFT JOIN orders ON users.id = orders.user_id
WHERE orders.id IS NULL;+------+
| name |
+------+
| 赵六 |
| 周八 |
| 吴九 |
| 郑十 |
+------+原理:左联结后,没订单的人对应的 orders.id 是 NULL,WHERE 一筛就出来。
⚠️ 注意用
orders.id IS NULL而不用orders.user_id IS NULL——如果 user_id 本身允许 NULL,判断会不准。用主键列(id)判断最可靠。
RIGHT JOIN:右表全保留
RIGHT JOIN 反过来——右表(JOIN 后面的表)的每一行都保留:
SELECT users.name, orders.product
FROM users
RIGHT JOIN orders ON users.id = orders.user_id;因为我们的 orders 表每一行都有对应的 user,所以 RIGHT JOIN 的结果和 INNER JOIN 一样。
RIGHT JOIN 的实际使用场景比 LEFT JOIN 少得多。实际上任何 RIGHT JOIN 都可以通过交换左右表变成 LEFT JOIN:
-- 这两句结果完全一样
FROM users RIGHT JOIN orders ON ...
FROM orders LEFT JOIN users ON ...💡 团队协作时统一用 LEFT JOIN 即可,少一个选择就少一份纠结。RIGHT JOIN 知道有这个东西就行。
表别名:让 SQL 更短
表名一长,SQL 变得又臭又长。给表起别名,省打字:
-- 不用别名
SELECT users.name, orders.product, orders.order_date
FROM users
INNER JOIN orders ON users.id = orders.user_id;
-- 用别名
SELECT u.name, o.product, o.order_date
FROM users AS u
INNER JOIN orders AS o ON u.id = o.user_id;AS 关键字甚至可以省略,但留着更清晰:
FROM users u
INNER JOIN orders o ON u.id = o.user_id⚠️ 一旦用了别名,整条 SQL 里必须用别名。写
SELECT users.name会报错——MySQL 已经不认识users这个原名了。
联结多张表
联结不限于两张表,可以一直往后接。比如再加一张 products 表存商品详情(价格、分类):
SELECT u.name, o.product, p.price, o.order_date
FROM users AS u
INNER JOIN orders AS o ON u.id = o.user_id
INNER JOIN products AS p ON o.product = p.name;一条一条往后 JOIN,每个 JOIN 带一个 ON 条件。MySQL 会从第一张表开始,逐个关联。
💡 没有硬性的联结数量上限,但一张 SQL 里 JOIN 超过四五张表时,可读性和性能都开始下降——该考虑拆成子查询或视图了。
JOIN vs 子查询,该用哪个?
子查询那章说"很多子查询可以用 JOIN 替代",现在可以看一个对比:
子查询方式——查所有下过单的人:
SELECT name FROM users
WHERE id IN (SELECT DISTINCT user_id FROM orders);JOIN 方式——同样的结果:
SELECT DISTINCT u.name
FROM users AS u
INNER JOIN orders AS o ON u.id = o.user_id;怎么选?一张表说清:
| 子查询 | JOIN | |
|---|---|---|
| 可读性 | 逻辑嵌套,套多了绕 | 平铺直叙,一行一 JOIN |
| 需要右表的数据 | ❌ 子查询只能给外层当条件用 | ✅ 可以同时 SELECT 两表的列 |
| 大数据性能 | 子查询可能每行重跑 | JOIN 通常更快 |
| 适用场景 | "查存在性"(有没有、在不在) | "要两表的数据合在一起" |
💡 简单经验:只要结果里需要两张表的列,就用 JOIN。只判断"存不存在",子查询更直观。
一张图总结三种 JOIN
INNER JOIN LEFT JOIN RIGHT JOIN
取交集 左表全保留 右表全保留
A ∩ B A ⟗ B A ⟖ B
┌──┬──┐ ┌──┬──┐ ┌──┬──┐
│██│ │ │██│ │ │██│ │
│██│ │ │██│ │ │██│ │
├──┤ │ ├──┤ │ ├──┤ │
│ │ │ │██│ │ │ │ │
│ │ │ │██│ │ │ │ │
└──┴──┘ └──┴──┘ └──┴──┘
只返回匹配 A全部保留 B全部保留小结
JOIN 是 SQL 最强大的功能之一——把分散在多张表的数据拼成一张完整视图:
- 分表是常态 — 正经项目不可能一张表塞所有数据,JOIN 是必须掌握的技能
- INNER JOIN 取交集 — 两边都匹配的行才返回,最常用
- LEFT JOIN 最实用 — 左表全保留,没匹配的填 NULL,查"谁没订单"就用它
- RIGHT JOIN 知道就行 — 用 LEFT JOIN 换一下表顺序就能实现
- 表别名 — 让 SQL 更短更清晰,自联结时别名是必须的
💡 联结不难,花 10 分钟把三种 JOIN 的图形在纸上画一遍,比看任何文字解释都管用。
自主练习
基于 users 表和 orders 表:
- 用 INNER JOIN 列出所有订单的产品和下单人城市
- 用 LEFT JOIN 列出所有人(包括没订单的),显示名字和订单数
- 找出下单超过 2 次的人的名字(提示:GROUP BY + HAVING + JOIN)
- 查出哪些产品从来没有人买过(提示:products 表 + LEFT JOIN + IS NULL)
下一章我们继续深入——自联结、自然联结、以及怎么把 JOIN 和聚合函数搭配使用。