主题
详细介绍一下 MySQL 中的 JOIN 操作,可以适当进行类比
一、JOIN 是什么?——从生活场景理解
假设你有两张纸条:
- 纸条 A(订单表):记录了每笔订单的编号、客户 ID、金额
- 纸条 B(客户表):记录了每个客户的 ID、姓名、电话
现在老板问你:"每笔订单是哪个客户下的?金额多少?客户电话是什么?"
你需要做的事情就是:根据「客户 ID」这个共同信息,把两张纸条的内容对照着拼在一起。这个"拼接"动作,就是 SQL 中的 JOIN。
纸条 A(orders) 纸条 B(customers)
┌─────────┬─────────┬───────┐ ┌─────────┬────────┬─────────────┐
│order_id │cust_id │amount │ │cust_id │name │phone │
├─────────┼─────────┼───────┤ ├─────────┼────────┼─────────────┤
│ 1001 │ C01 │ 200 │ │ C01 │ 张三 │ 138xxxx1111 │
│ 1002 │ C02 │ 350 │ │ C02 │ 李四 │ 139xxxx2222 │
│ 1003 │ C01 │ 120 │ │ C03 │ 王五 │ 137xxxx3333 │
│ 1004 │ C99 │ 80 │ └─────────┴────────┴─────────────┘
└─────────┴─────────┴───────┘ ↑ C03 没有订单
↑ C99 在客户表中不存在
JOIN
↓
"把两张纸条按 cust_id 对齐,拼成一张大表"核心本质:JOIN 就是在两张(或多张)表之间,通过一个关联条件(通常是外键 = 主键),将相关的行"横向拼接"在一起。
二、JOIN 的类型——用集合的思维理解
可以把两张表看成两个圆(集合 A 和集合 B),JOIN 的不同类型就是取这两个圆的不同部分:
集合 A (左表) 集合 B (右表)
┌──────────┐ ┌──────────┐
│ │ │ │
│ A │ │ B │
│ only ├──────────┤ only │
│ │ A ∩ B │ │
│ │ (交集) │ │
└──────────┴──────────┴──────────┘
┌──────────────────────────────────────────────────────────────┐
│ INNER JOIN → 只取交集部分 (A ∩ B) │
│ LEFT JOIN → 左表全部 + 交集 (A + A∩B) │
│ RIGHT JOIN → 右表全部 + 交集 (B + A∩B) │
│ FULL OUTER JOIN→ 两表全部 (A + B) — MySQL 不直接支持 │
│ CROSS JOIN → 笛卡尔积 (A × B) │
│ SELF JOIN → 自己和自己连接 │
└──────────────────────────────────────────────────────────────┘三、各种 JOIN 详细解析
示例数据准备
以下所有示例基于这两张表:
sql
-- 学生表
CREATE TABLE students (
id INT PRIMARY KEY,
name VARCHAR(50),
class_id INT
);
INSERT INTO students VALUES
(1, '小明', 101),
(2, '小红', 102),
(3, '小刚', 101),
(4, '小美', NULL), -- 没有分配班级
(5, '小李', 103);
-- 班级表
CREATE TABLE classes (
id INT PRIMARY KEY,
class_name VARCHAR(50),
teacher VARCHAR(50)
);
INSERT INTO classes VALUES
(101, '一班', '张老师'),
(102, '二班', '李老师'),
(104, '四班', '赵老师'); -- 没有学生选这个班students 表 classes 表
┌────┬──────┬──────────┐ ┌─────┬────────────┬────────┐
│ id │ name │ class_id │ │ id │ class_name │teacher │
├────┼──────┼──────────┤ ├─────┼────────────┼────────┤
│ 1 │ 小明 │ 101 │ │ 101 │ 一班 │张老师 │
│ 2 │ 小红 │ 102 │ │ 102 │ 二班 │李老师 │
│ 3 │ 小刚 │ 101 │ │ 104 │ 四班 │赵老师 │ ← 没有学生
│ 4 │ 小美 │ NULL │ └─────┴────────────┴────────┘
│ 5 │ 小李 │ 103 │ ← 103 不存在
└────┴──────┴──────────┘3.1 INNER JOIN(内连接)——只要"双方都有的"
类比:相亲大会上,男嘉宾和女嘉宾配对成功的才留下,没匹配上的都淘汰。
规则:只返回左表和右表中都能匹配上的行。
sql
SELECT
s.name AS student_name,
c.class_name,
c.teacher
FROM students s
INNER JOIN classes c ON s.class_id = c.id;执行过程(逐行扫描):
s.id=1, s.class_id=101 → classes 中有 id=101 → ✅ 匹配,拼接输出
s.id=2, s.class_id=102 → classes 中有 id=102 → ✅ 匹配,拼接输出
s.id=3, s.class_id=101 → classes 中有 id=101 → ✅ 匹配,拼接输出
s.id=4, s.class_id=NULL → classes 中没有 NULL → ❌ 丢弃
s.id=5, s.class_id=103 → classes 中没有 103 → ❌ 丢弃结果:
┌──────────────┬────────────┬─────────┐
│ student_name │ class_name │ teacher │
├──────────────┼────────────┼─────────┤
│ 小明 │ 一班 │ 张老师 │
│ 小红 │ 二班 │ 李老师 │
│ 小刚 │ 一班 │ 张老师 │
└──────────────┴────────────┴─────────┘
只有 3 行:小美(NULL) 和 小李(103) 因为没有匹配的班级被过滤掉了。关键特征:INNER JOIN 结果行数 ≤ min(左表行数, 右表行数)。任何一边没有匹配就不会出现。
3.2 LEFT JOIN(左外连接)——"左边一个不能少"
类比:班级点名。不管你有没有来(右表有没有匹配),花名册上的人(左表)一个都不能少。没来的在签到栏写"缺席"(NULL)。
规则:返回左表的所有行,右表没有匹配的部分用 NULL 填充。
sql
SELECT
s.name AS student_name,
c.class_name,
c.teacher
FROM students s
LEFT JOIN classes c ON s.class_id = c.id;结果:
┌──────────────┬────────────┬─────────┐
│ student_name │ class_name │ teacher │
├──────────────┼────────────┼─────────┤
│ 小明 │ 一班 │ 张老师 │
│ 小红 │ 二班 │ 李老师 │
│ 小刚 │ 一班 │ 张老师 │
│ 小美 │ NULL │ NULL │ ← 左表保留,右表填 NULL
│ 小李 │ NULL │ NULL │ ← 左表保留,右表填 NULL
└──────────────┴────────────┴─────────┘
5 行全部返回,左表一个不少。常见用法:找出"没有匹配的"
sql
-- 找出没有分配到有效班级的学生
SELECT s.name
FROM students s
LEFT JOIN classes c ON s.class_id = c.id
WHERE c.id IS NULL;
-- 结果:小美、小李这个模式非常实用,相当于求 A - A∩B(左表独有的部分)。
3.3 RIGHT JOIN(右外连接)——"右边一个不能少"
类比:和 LEFT JOIN 完全对称。这次是"教室点名"——不管教室里有没有人(左表有没有匹配),每间教室(右表)都要登记。
规则:返回右表的所有行,左表没有匹配的部分用 NULL 填充。
sql
SELECT
s.name AS student_name,
c.class_name,
c.teacher
FROM students s
RIGHT JOIN classes c ON s.class_id = c.id;结果:
┌──────────────┬────────────┬─────────┐
│ student_name │ class_name │ teacher │
├──────────────┼────────────┼─────────┤
│ 小明 │ 一班 │ 张老师 │
│ 小刚 │ 一班 │ 张老师 │
│ 小红 │ 二班 │ 李老师 │
│ NULL │ 四班 │ 赵老师 │ ← 右表保留,左表填 NULL
└──────────────┴────────────┴─────────┘
四班没有学生,但依然出现在结果中。实际建议:RIGHT JOIN 可以通过交换表位置改写成 LEFT JOIN。为了代码一致性,建议统一使用 LEFT JOIN。
上面的 RIGHT JOIN 等价于:
sql
SELECT
s.name AS student_name,
c.class_name,
c.teacher
FROM classes c
LEFT JOIN students s ON s.class_id = c.id;3.4 FULL OUTER JOIN(全外连接)——"两边全都要"
类比:同学聚会,既要统计所有参加的人,也要统计所有被邀请但没来的人。反正所有相关的人都列出来。
规则:返回左表和右表的所有行,没有匹配的都用 NULL 填充。
⚠️ MySQL 不直接支持
FULL OUTER JOIN,需要用UNION模拟:
sql
-- 模拟 FULL OUTER JOIN
SELECT s.name, c.class_name, c.teacher
FROM students s
LEFT JOIN classes c ON s.class_id = c.id
UNION
SELECT s.name, c.class_name, c.teacher
FROM students s
RIGHT JOIN classes c ON s.class_id = c.id;结果:
┌──────┬────────────┬─────────┐
│ name │ class_name │ teacher │
├──────┼────────────┼─────────┤
│ 小明 │ 一班 │ 张老师 │
│ 小红 │ 二班 │ 李老师 │
│ 小刚 │ 一班 │ 张老师 │
│ 小美 │ NULL │ NULL │ ← 左表独有
│ 小李 │ NULL │ NULL │ ← 左表独有
│ NULL │ 四班 │ 赵老师 │ ← 右表独有
└──────┴────────────┴─────────┘3.5 CROSS JOIN(交叉连接 / 笛卡尔积)——"所有组合"
类比:你有 3 件上衣和 4 条裤子,CROSS JOIN 就是列出所有可能的穿搭组合 = 3 × 4 = 12 种。
规则:左表的每一行 × 右表的每一行,没有 ON 条件。
sql
SELECT s.name, c.class_name
FROM students s
CROSS JOIN classes c;
-- 5 个学生 × 3 个班级 = 15 行结果(部分):
┌──────┬────────────┐
│ name │ class_name │
├──────┼────────────┤
│ 小明 │ 一班 │
│ 小明 │ 二班 │
│ 小明 │ 四班 │
│ 小红 │ 一班 │
│ 小红 │ 二班 │
│ 小红 │ 四班 │
│ ... │ ... │ (共 15 行)
└──────┴────────────┘应用场景
sql
-- 生成日历 × 商品的组合(用于统计每天每个商品是否有销售)
SELECT d.date, p.product_name
FROM dates d
CROSS JOIN products p;
-- 生成所有可能的对战组合
SELECT a.team_name AS home, b.team_name AS away
FROM teams a
CROSS JOIN teams b
WHERE a.id != b.id;⚠️ 注意:CROSS JOIN 产生的行数 = 左表行数 × 右表行数。两张大表做 CROSS JOIN 会产生天文数字的行数,非常危险。
3.6 SELF JOIN(自连接)——"自己跟自己比"
类比:一个公司的组织架构图——每个人既是"员工",也可能是别人的"上级"。同一张表,要用两个身份来关联。
规则:同一张表使用不同的别名,然后 JOIN 自身。
sql
CREATE TABLE employees (
id INT PRIMARY KEY,
name VARCHAR(50),
manager_id INT,
salary DECIMAL(10, 2)
);
INSERT INTO employees VALUES
(1, '总经理', NULL, 100000),
(2, '技术总监', 1, 80000),
(3, '销售总监', 1, 75000),
(4, '高级开发', 2, 60000),
(5, '初级开发', 2, 40000);sql
-- 查询每个员工的上级是谁
SELECT
e.name AS employee,
m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;┌──────────┬──────────┐
│ employee │ manager │
├──────────┼──────────┤
│ 总经理 │ NULL │ ← 没有上级
│ 技术总监 │ 总经理 │
│ 销售总监 │ 总经理 │
│ 高级开发 │ 技术总监 │
│ 初级开发 │ 技术总监 │
└──────────┴──────────┘sql
-- 找出工资高于上级的员工
SELECT
e.name AS employee,
e.salary AS emp_salary,
m.name AS manager,
m.salary AS mgr_salary
FROM employees e
INNER JOIN employees m ON e.manager_id = m.id
WHERE e.salary > m.salary;四、JOIN 的底层执行原理
理解 JOIN 怎么执行的,有助于写出高性能的 SQL。
4.1 三种执行算法
┌────────────────────────────────────────────────────────────────┐
│ MySQL JOIN 执行算法 │
├─────────────────┬──────────────────────────────────────────────┤
│ │ │
│ Simple Nested │ 最朴素的方式: │
│ Loop Join │ 遍历左表每一行,对每行都全表扫描右表 │
│ (简单嵌套循环) │ 时间复杂度:O(M × N) │
│ │ 类比:在电话簿里一页一页翻找每个人 │
│ │ │
├─────────────────┼──────────────────────────────────────────────┤
│ │ │
│ Index Nested │ 如果右表关联列有索引: │
│ Loop Join │ 遍历左表每一行,通过索引快速查找右表 │
│ (索引嵌套循环) │ 时间复杂度:O(M × logN) │
│ │ 类比:在电话簿里直接查目录找到人 │
│ │ │
├─────────────────┼──────────────────────────────────────────────┤
│ │ │
│ Block Nested │ 将左表数据分批加载到内存(Join Buffer): │
│ Loop Join │ 一次比对一批而不是一行 │
│ (块嵌套循环) │ 减少右表的扫描次数 │
│ │ 类比:把一摞简历摊开,一次对照多份 │
│ │ │
├─────────────────┼──────────────────────────────────────────────┤
│ │ │
│ Hash Join │ MySQL 8.0.18+ 新增: │
│ (哈希连接) │ 将小表构建 Hash 表,大表逐行探测匹配 │
│ │ 时间复杂度:O(M + N) │
│ │ 类比:先把一张表做成字典,另一张表查字典 │
│ │ │
└─────────────────┴──────────────────────────────────────────────┘4.2 驱动表与被驱动表
执行 A JOIN B ON A.key = B.key 时:
驱动表(外层循环) 被驱动表(内层循环)
┌──────────┐ ┌──────────┐
│ Table A │──遍历──→ │ Table B │
│ (小表) │ 每一行 │ (大表) │ ← 被驱动表应该有索引
└──────────┘ └──────────┘
优化器通常选小表作为驱动表(减少外层循环次数)
被驱动表的关联列需要有索引(加速内层查找)口诀:小表驱动大表,大表建索引。
五、JOIN 的执行顺序与 ON vs WHERE
5.1 执行顺序
FROM / JOIN ← 第 1 步:确定表和连接方式
↓
ON 条件过滤 ← 第 2 步:按 ON 条件匹配行
↓
添加外表行 ← 第 3 步:LEFT/RIGHT JOIN 会把未匹配的行加回来(填 NULL)
↓
WHERE 过滤 ← 第 4 步:对 JOIN 结果进一步过滤
↓
GROUP BY → HAVING → SELECT → ORDER BY → LIMIT5.2 ON 和 WHERE 的区别(非常重要!)
对于 INNER JOIN,ON 和 WHERE 效果相同。但对于 LEFT JOIN,区别巨大:
sql
-- 示例:查找一班的学生
-- 写法 1:条件放在 ON 里
SELECT s.name, c.class_name
FROM students s
LEFT JOIN classes c ON s.class_id = c.id AND c.id = 101;
-- 结果:
-- ┌──────┬────────────┐
-- │ name │ class_name │
-- ├──────┼────────────┤
-- │ 小明 │ 一班 │ ← ON 匹配成功
-- │ 小红 │ NULL │ ← ON 匹配失败,但 LEFT JOIN 保留左表行
-- │ 小刚 │ 一班 │ ← ON 匹配成功
-- │ 小美 │ NULL │ ← 保留
-- │ 小李 │ NULL │ ← 保留
-- └──────┴────────────┘
-- 返回 5 行(所有学生都在)
-- 写法 2:条件放在 WHERE 里
SELECT s.name, c.class_name
FROM students s
LEFT JOIN classes c ON s.class_id = c.id
WHERE c.id = 101;
-- 结果:
-- ┌──────┬────────────┐
-- │ name │ class_name │
-- ├──────┼────────────┤
-- │ 小明 │ 一班 │
-- │ 小刚 │ 一班 │
-- └──────┴────────────┘
-- 返回 2 行(WHERE 在 JOIN 之后执行,把不符合的都过滤了)总结:
ON 条件:决定 JOIN 时如何匹配,不影响 LEFT JOIN 保留左表全部行
WHERE 条件:在 JOIN 结果上再次过滤,会去掉不满足条件的行
INNER JOIN → ON / WHERE 效果一样
LEFT JOIN → ON 不会减少左表行数,WHERE 会六、多表 JOIN——链式连接
实际业务中经常需要连接三张甚至更多的表。
sql
-- 三表连接:查询订单详情(订单 → 客户 → 地址)
SELECT
o.order_id,
c.name AS customer_name,
a.city,
o.amount
FROM orders o
INNER JOIN customers c ON o.customer_id = c.id
INNER JOIN addresses a ON c.address_id = a.id
WHERE o.amount > 100;执行流程:
Step 1: orders JOIN customers → 产生临时结果集 T1
Step 2: T1 JOIN addresses → 产生临时结果集 T2
Step 3: 对 T2 执行 WHERE 过滤 → 最终结果注意事项:
- 每多一次 JOIN,数据量可能会膨胀(一对多关系时)
- 建议 JOIN 的表不超过 3~5 张,超过的考虑拆分查询或使用临时表
- 每个 JOIN 的关联列都应该有索引
七、JOIN 性能优化
7.1 优化清单
┌─────┬──────────────────────────────────────────────────────┐
│ 1 │ 被驱动表的 JOIN 列必须建索引 │
│ │ ALTER TABLE orders ADD INDEX idx_cust_id(cust_id); │
├─────┼──────────────────────────────────────────────────────┤
│ 2 │ 小表驱动大表(优化器通常自动选择) │
│ │ 可通过 STRAIGHT_JOIN 强制指定驱动表顺序 │
├─────┼──────────────────────────────────────────────────────┤
│ 3 │ 避免 SELECT *,只查需要的列 │
│ │ 减少数据传输量和 Join Buffer 的内存占用 │
├─────┼──────────────────────────────────────────────────────┤
│ 4 │ 增大 join_buffer_size │
│ │ 对于无法使用索引的 JOIN,更大的 Buffer 可减少扫描次数 │
├─────┼──────────────────────────────────────────────────────┤
│ 5 │ 关联列类型和字符集必须一致 │
│ │ 类型不一致会导致隐式转换,索引失效 │
├─────┼──────────────────────────────────────────────────────┤
│ 6 │ 用 EXPLAIN 检查执行计划 │
│ │ 关注 type 列:eq_ref > ref > ALL │
│ │ 出现 ALL 说明是全表扫描,需要优化 │
├─────┼──────────────────────────────────────────────────────┤
│ 7 │ 避免在 ON 条件中使用函数 │
│ │ ON YEAR(a.date) = b.year → 索引失效 │
│ │ 改为 ON a.date BETWEEN '2026-01-01' AND '2026-12-31'│
└─────┴──────────────────────────────────────────────────────┘7.2 用 EXPLAIN 分析 JOIN
sql
EXPLAIN SELECT s.name, c.class_name
FROM students s
INNER JOIN classes c ON s.class_id = c.id;┌────┬─────────┬───────┬────────┬──────────────┬──────┬─────────┐
│ id │ table │ type │ key │ key_len │ rows │ Extra │
├────┼─────────┼───────┼────────┼──────────────┼──────┼─────────┤
│ 1 │ classes │ ALL │ NULL │ NULL │ 3 │ NULL │
│ 1 │ students│ ref │ idx_cls│ 5 │ 2 │ NULL │
└────┴─────────┴───────┴────────┴──────────────┴──────┴─────────┘
关注点:
- type=ALL → 全表扫描(classes 表小,优化器选它做驱动表)
- type=ref → 通过索引查找(students 表通过 class_id 索引定位)
- rows → 预估扫描行数八、JOIN 的七种经典用法(维恩图总结)
1. INNER JOIN 2. LEFT JOIN 3. RIGHT JOIN
A ∩ B A B
╭──╮ ╭──╮ ╭──╮ ╭──╮ ╭──╮ ╭──╮
│ ├██┤ │ │██├██┤ │ │ ├██┤██│
╰──╯ ╰──╯ ╰──╯ ╰──╯ ╰──╯ ╰──╯
4. LEFT EXCLUSIVE 5. RIGHT EXCLUSIVE 6. FULL OUTER JOIN
A - B B - A A ∪ B
╭──╮ ╭──╮ ╭──╮ ╭──╮ ╭──╮ ╭──╮
│██├──┤ │ │ ├──┤██│ │██├██┤██│
╰──╯ ╰──╯ ╰──╯ ╰──╯ ╰──╯ ╰──╯
7. FULL EXCLUSIVE (A - B) ∪ (B - A)
╭──╮ ╭──╮
│██├──┤██│
╰──╯ ╰──╯对应 SQL:
sql
-- 1. INNER JOIN: A ∩ B
SELECT * FROM A INNER JOIN B ON A.key = B.key;
-- 2. LEFT JOIN: A(包含交集)
SELECT * FROM A LEFT JOIN B ON A.key = B.key;
-- 3. RIGHT JOIN: B(包含交集)
SELECT * FROM A RIGHT JOIN B ON A.key = B.key;
-- 4. LEFT EXCLUSIVE: A - B(A 独有的)
SELECT * FROM A LEFT JOIN B ON A.key = B.key WHERE B.key IS NULL;
-- 5. RIGHT EXCLUSIVE: B - A(B 独有的)
SELECT * FROM A RIGHT JOIN B ON A.key = B.key WHERE A.key IS NULL;
-- 6. FULL OUTER JOIN: A ∪ B
SELECT * FROM A LEFT JOIN B ON A.key = B.key
UNION
SELECT * FROM A RIGHT JOIN B ON A.key = B.key;
-- 7. FULL EXCLUSIVE: (A - B) ∪ (B - A)
SELECT * FROM A LEFT JOIN B ON A.key = B.key WHERE B.key IS NULL
UNION
SELECT * FROM A RIGHT JOIN B ON A.key = B.key WHERE A.key IS NULL;九、常见面试题与陷阱
Q1: INNER JOIN 和 LEFT JOIN 的区别?
| 对比项 | INNER JOIN | LEFT JOIN |
|---|---|---|
| 匹配行为 | 只返回两表都匹配的行 | 返回左表全部行,右表不匹配填 NULL |
| 结果行数 | ≤ min(左表, 右表) | = 左表行数(一对一时) |
| NULL 值 | 不会产生因 JOIN 而来的 NULL | 右表列可能为 NULL |
| 使用场景 | 只需要有关联的数据 | 需要保留左表全部记录 |
Q2: ON 和 WHERE 的区别?
- INNER JOIN 中:ON 和 WHERE 效果一样,因为不匹配的行最终都会被丢弃。
- LEFT JOIN 中:ON 中的条件不会过滤掉左表的行(不匹配的行以 NULL 形式保留);WHERE 中的条件会在 JOIN 之后过滤,可能会移除左表的行。
- 记忆口诀:ON 管"怎么连",WHERE 管"连完后留谁"。
Q3: 如何避免笛卡尔积?
sql
-- ❌ 忘写 ON 条件 → 笛卡尔积
SELECT * FROM students, classes; -- 5 × 3 = 15 行
SELECT * FROM students CROSS JOIN classes; -- 同上
-- ❌ ON 条件逻辑错误
SELECT * FROM students s
JOIN classes c ON s.id = c.id; -- 语义上不对
-- ✅ 正确写法
SELECT * FROM students s
JOIN classes c ON s.class_id = c.id;
-- 排查方法:结果行数远超预期时,检查 ON 条件是否正确Q4: JOIN 和子查询(Subquery)怎么选?
sql
-- 场景:查找有学生的班级
-- 方式 1:JOIN(推荐,优化器容易处理)
SELECT DISTINCT c.class_name
FROM classes c
INNER JOIN students s ON c.id = s.class_id;
-- 方式 2:EXISTS(适合大表判断存在性)
SELECT c.class_name FROM classes c
WHERE EXISTS (SELECT 1 FROM students s WHERE s.class_id = c.id);
-- 方式 3:IN(适合子查询结果集小的场景)
SELECT c.class_name FROM classes c
WHERE c.id IN (SELECT class_id FROM students WHERE class_id IS NOT NULL);选择原则:
- 能用 JOIN 就用 JOIN(性能好、可读性强)
- 判断"是否存在"用 EXISTS
- 子查询结果集小用 IN
- 需要复用中间结果用 CTE (
WITH ... AS)
Q5: 一对多 JOIN 结果行数会膨胀吗?
会的! 这是一个常见的坑。
sql
-- 假设一个作者写了 5 本书
-- authors 表 1 行 × books 表 5 行 = JOIN 结果 5 行
-- ❌ 直接 COUNT(*) 会得到错误的作者数
SELECT COUNT(*) FROM authors a JOIN books b ON a.id = b.author_id;
-- 结果不是作者数,而是作者-书籍配对数
-- ✅ 正确做法
SELECT COUNT(DISTINCT a.id) FROM authors a JOIN books b ON a.id = b.author_id;十、JOIN 实战速查表
┌──────────────────────────────────────────────────────────────────┐
│ JOIN 速查表 │
├──────────────────┬───────────────────────────────────────────────┤
│ │ │
│ INNER JOIN │ SELECT * FROM A JOIN B ON A.k = B.k │
│ │ 只返回匹配行 │
│ │ │
│ LEFT JOIN │ SELECT * FROM A LEFT JOIN B ON A.k = B.k │
│ │ 左表全部,右表不匹配填 NULL │
│ │ │
│ LEFT EXCLUSIVE │ ... LEFT JOIN ... WHERE B.k IS NULL │
│ │ 只返回左表独有的行 │
│ │ │
│ CROSS JOIN │ SELECT * FROM A CROSS JOIN B │
│ │ 笛卡尔积 (M × N 行) │
│ │ │
│ SELF JOIN │ SELECT * FROM A a1 JOIN A a2 ON a1.x = a2.y │
│ │ 自己和自己连接 │
│ │ │
├──────────────────┼───────────────────────────────────────────────┤
│ │ │
│ 性能优化 │ 1. 被驱动表 JOIN 列建索引 │
│ │ 2. 小表驱动大表 │
│ │ 3. 避免 SELECT * │
│ │ 4. EXPLAIN 检查执行计划 │
│ │ 5. 关联列类型必须一致 │
│ │ │
├──────────────────┼───────────────────────────────────────────────┤
│ │ │
│ 易错点 │ 1. 忘写 ON → 笛卡尔积 │
│ │ 2. LEFT JOIN + WHERE 过滤右表 → 变成 INNER │
│ │ 3. 一对多 JOIN 行数膨胀 │
│ │ 4. ON 中用函数 → 索引失效 │
│ │ │
└──────────────────┴───────────────────────────────────────────────┘📝 学习建议:先彻底理解 INNER JOIN 和 LEFT JOIN(覆盖 90% 的场景),再掌握 SELF JOIN 和 CROSS JOIN。遇到复杂 JOIN 时,先画出表之间的关系图,明确"谁是左表、谁是右表、关联列是什么、需要保留哪边的全部数据",SQL 就容易写了。