SQL 连接是关系型数据库的核心。它允许你根据关联列将两个或多个表的数据组合在一起。尽管连接是基础概念,但对很多开发者来说仍然令人困惑。本指南通过可视化图表和实际示例,让每种连接类型都变得清晰易懂。
示例数据:两张表
本指南中,我们将使用以下两张简单的表:
-- employees table
+----+----------+---------------+
| id | name | department_id |
+----+----------+---------------+
| 1 | Alice | 1 |
| 2 | Bob | 2 |
| 3 | Charlie | 1 |
| 4 | Diana | NULL |
+----+----------+---------------+
-- departments table
+----+-------------+
| id | dept_name |
+----+-------------+
| 1 | Engineering |
| 2 | Marketing |
| 3 | Sales |
+----+-------------+INNER JOIN(内连接)
只返回两张表中都有匹配值的行。可以理解为两个集合的交集。
Table A Table B
+---------+ +---------+
| | | |
| +----+-----+----+ |
| | INNER JOIN | |
| +----+-----+----+ |
| | | |
+---------+ +---------+SELECT e.name, d.dept_name
FROM employees e
INNER JOIN departments d ON e.department_id = d.id;结果:
+----------+-------------+
| name | dept_name |
+----------+-------------+
| Alice | Engineering |
| Bob | Marketing |
| Charlie | Engineering |
+----------+-------------+
-- Diana excluded (department_id is NULL)
-- Sales excluded (no employee in Sales)当你只需要两张表中都存在的记录时使用。
LEFT JOIN(左外连接)
返回左表的所有行和右表的匹配行。如果没有匹配,右表列返回 NULL。
Table A Table B
+---------+ +---------+
|XXXXXXXXX| | |
|XXXX+----+-----+----+ |
|XXXX| LEFT JOIN XXXX| |
|XXXX+----+-----+----+ |
|XXXXXXXXX| | |
+---------+ +---------+
(all of A + matching B)SELECT e.name, d.dept_name
FROM employees e
LEFT JOIN departments d ON e.department_id = d.id;结果:
+----------+-------------+
| name | dept_name |
+----------+-------------+
| Alice | Engineering |
| Bob | Marketing |
| Charlie | Engineering |
| Diana | NULL |
+----------+-------------+
-- Diana included with NULL dept_name当你需要左表的所有记录时使用,不管它们是否有匹配。
RIGHT JOIN(右外连接)
返回右表的所有行和左表的匹配行。如果没有匹配,左表列返回 NULL。
Table A Table B
+---------+ +---------+
| | |XXXXXXXXX|
| +----+-----+XXXX+XXXX|
| |XXXX RIGHT JOINXXXX|
| +----+-----+XXXX+XXXX|
| | |XXXXXXXXX|
+---------+ +---------+
(matching A + all of B)SELECT e.name, d.dept_name
FROM employees e
RIGHT JOIN departments d ON e.department_id = d.id;结果:
+----------+-------------+
| name | dept_name |
+----------+-------------+
| Alice | Engineering |
| Charlie | Engineering |
| Bob | Marketing |
| NULL | Sales |
+----------+-------------+
-- Sales included with NULL name当你需要右表的所有记录时使用。
FULL OUTER JOIN(全外连接)
返回两张表的所有行。没有匹配的地方用 NULL 填充。
Table A Table B
+---------+ +---------+
|XXXXXXXXX| |XXXXXXXXX|
|XXXX+----+-----+XXXX+XXXX|
|XXXX|FULL OUTER JOINXXXX|
|XXXX+----+-----+XXXX+XXXX|
|XXXXXXXXX| |XXXXXXXXX|
+---------+ +---------+
(everything from both)SELECT e.name, d.dept_name
FROM employees e
FULL OUTER JOIN departments d ON e.department_id = d.id;结果:
+----------+-------------+
| name | dept_name |
+----------+-------------+
| Alice | Engineering |
| Bob | Marketing |
| Charlie | Engineering |
| Diana | NULL |
| NULL | Sales |
+----------+-------------+当你需要两张表的所有记录,包括不匹配的行时使用。
CROSS JOIN(交叉连接)
返回两张表的笛卡尔积——左表的每一行与右表的每一行组合。不需要 ON 子句。
SELECT e.name, d.dept_name
FROM employees e
CROSS JOIN departments d;
-- Returns 4 × 3 = 12 rows结果:
+----------+-------------+
| name | dept_name |
+----------+-------------+
| Alice | Engineering |
| Alice | Marketing |
| Alice | Sales |
| Bob | Engineering |
| Bob | Marketing |
| Bob | Sales |
| Charlie | Engineering |
| Charlie | Marketing |
| Charlie | Sales |
| Diana | Engineering |
| Diana | Marketing |
| Diana | Sales |
+----------+-------------+用于生成组合、测试数据或日历网格。避免在大表上使用。
自连接(Self JOIN)
表与自身连接。适用于层级数据或同一表内的行比较。
-- employees_v2 with manager_id column
+----+---------+------------+
| id | name | manager_id |
+----+---------+------------+
| 1 | Alice | NULL |
| 2 | Bob | 1 |
| 3 | Charlie | 1 |
| 4 | Diana | 2 |
+----+---------+------------+
SELECT
e.name AS employee,
m.name AS manager
FROM employees_v2 e
LEFT JOIN employees_v2 m ON e.manager_id = m.id;结果:
+----------+---------+
| employee | manager |
+----------+---------+
| Alice | NULL |
| Bob | Alice |
| Charlie | Alice |
| Diana | Bob |
+----------+---------+用于员工-经理关系、层级分类或行间比较。
用哪种连接?速查表
| 连接类型 | 返回内容 | 最佳使用场景 |
|---|---|---|
| INNER JOIN | A ∩ B | 只要匹配数据 |
| LEFT JOIN | A + (A ∩ B) | 所有左表 + 匹配右表 |
| RIGHT JOIN | (A ∩ B) + B | 所有右表 + 匹配左表 |
| FULL OUTER JOIN | A ∪ B | 两表所有数据 |
| CROSS JOIN | A × B | 生成所有组合 |
| Self JOIN | 取决于连接类型 | 层级/自引用数据 |
性能优化技巧
- 始终为 JOIN 列创建索引——这是最大的性能提升。
- 生产环境避免 SELECT *,只查询需要的列。
- 尽量将小表放在 FROM 子句前面(虽然大多数优化器会自动处理)。
- 使用 EXPLAIN ANALYZE 检查查询执行计划。
- 对关联子查询优先使用 EXISTS 而不是 IN——它在第一次匹配时就会停止。
- 避免在表达式或函数上连接(如 JOIN ON LOWER(a.name) = LOWER(b.name))——无法使用索引。
-- Create index on JOIN column
CREATE INDEX idx_emp_dept ON employees(department_id);
-- Use EXPLAIN to check query plan
EXPLAIN ANALYZE
SELECT e.name, d.dept_name
FROM employees e
INNER JOIN departments d ON e.department_id = d.id;常见错误
| 错误 | 问题 | 修复方法 |
|---|---|---|
| 使用 SELECT * | 返回不必要的列,浪费带宽和内存 | 只选择需要的列 |
| 缺少 JOIN 列索引 | 全表扫描导致查询极慢 | CREATE INDEX |
| NULL 比较陷阱 | NULL = NULL 结果为 FALSE | 使用 IS NULL 或 COALESCE |
| 笛卡尔积意外 | 缺少 ON 条件导致行数爆炸 | 始终检查 ON 子句 |
| 在 WHERE 中过滤 OUTER JOIN | WHERE 过滤会把 LEFT JOIN 变成 INNER JOIN | 将条件放在 ON 子句中 |
| 多余的 DISTINCT | 掩盖了错误的 JOIN 逻辑 | 修复 JOIN 条件而非加 DISTINCT |
| 在函数上 JOIN | 索引无法使用,强制全表扫描 | 存储规范化数据或使用计算列 |
-- ❌ Wrong: WHERE turns LEFT JOIN into INNER JOIN
SELECT e.name, d.dept_name
FROM employees e
LEFT JOIN departments d ON e.department_id = d.id
WHERE d.dept_name = 'Engineering'; -- filters out NULLs!
-- ✅ Correct: Put condition in ON clause
SELECT e.name, d.dept_name
FROM employees e
LEFT JOIN departments d
ON e.department_id = d.id
AND d.dept_name = 'Engineering'; -- preserves NULLs常见问题
INNER JOIN 和 OUTER JOIN 有什么区别?
INNER JOIN 只返回两张表中匹配的行。OUTER JOIN(LEFT、RIGHT 或 FULL)还包含一个或两个表中不匹配的行,用 NULL 填充缺失值。
可以连接两张以上的表吗?
可以。你可以链式使用多个 JOIN。例如:SELECT * FROM A JOIN B ON A.id = B.a_id JOIN C ON B.id = C.b_id。没有实际限制。
INNER JOIN 忘记写 ON 子句会怎样?
在大多数 SQL 方言中,INNER JOIN 缺少 ON 子句会导致语法错误。在某些方言中,可能产生 CROSS JOIN(笛卡尔积),返回大量行。
LEFT JOIN 和 LEFT OUTER JOIN 一样吗?
是的。OUTER 关键字是可选的。在所有主流 SQL 数据库中,LEFT JOIN 和 LEFT OUTER JOIN 完全相同。
哪种连接最快?
INNER JOIN 通常最快,因为它只处理匹配的行。但实际性能取决于索引、表大小和查询优化器。始终使用 EXPLAIN 测量。
什么是 NATURAL JOIN?
NATURAL JOIN 自动在两个表中同名的列上进行连接。生产环境中通常不建议使用,因为添加列可能会悄悄改变连接行为。