Skip to content

联结表

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 库里直接跑:

sql
-- 建表
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 是最常用的联结——只返回两表中能匹配上的行

基本语法

把订单和用户名合在一起查:

sql
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_id
  • SELECT 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:

sql
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 精准捞出"沉默用户":

sql
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 后面的表)的每一行都保留

sql
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

sql
-- 这两句结果完全一样
FROM users RIGHT JOIN orders ON ...
FROM orders LEFT JOIN users ON ...

💡 团队协作时统一用 LEFT JOIN 即可,少一个选择就少一份纠结。RIGHT JOIN 知道有这个东西就行。

表别名:让 SQL 更短

表名一长,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 关键字甚至可以省略,但留着更清晰:

sql
FROM users u
INNER JOIN orders o ON u.id = o.user_id

⚠️ 一旦用了别名,整条 SQL 里必须用别名。写 SELECT users.name 会报错——MySQL 已经不认识 users 这个原名了。

联结多张表

联结不限于两张表,可以一直往后接。比如再加一张 products 表存商品详情(价格、分类):

sql
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 替代",现在可以看一个对比:

子查询方式——查所有下过单的人:

sql
SELECT name FROM users
WHERE id IN (SELECT DISTINCT user_id FROM orders);

JOIN 方式——同样的结果:

sql
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 最强大的功能之一——把分散在多张表的数据拼成一张完整视图:

  1. 分表是常态 — 正经项目不可能一张表塞所有数据,JOIN 是必须掌握的技能
  2. INNER JOIN 取交集 — 两边都匹配的行才返回,最常用
  3. LEFT JOIN 最实用 — 左表全保留,没匹配的填 NULL,查"谁没订单"就用它
  4. RIGHT JOIN 知道就行 — 用 LEFT JOIN 换一下表顺序就能实现
  5. 表别名 — 让 SQL 更短更清晰,自联结时别名是必须的

💡 联结不难,花 10 分钟把三种 JOIN 的图形在纸上画一遍,比看任何文字解释都管用。

自主练习

基于 users 表和 orders 表:

  1. 用 INNER JOIN 列出所有订单的产品和下单人城市
  2. 用 LEFT JOIN 列出所有人(包括没订单的),显示名字和订单数
  3. 找出下单超过 2 次的人的名字(提示:GROUP BY + HAVING + JOIN)
  4. 查出哪些产品从来没有人买过(提示:products 表 + LEFT JOIN + IS NULL)

下一章我们继续深入——自联结、自然联结、以及怎么把 JOIN 和聚合函数搭配使用。


评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.8