📌 一句话总结
JOIN 是 SQL 跨表查数据的核心机制,通过关联条件把多张表的行拼成结果集。掌握 INNER/LEFT/RIGHT/FULL JOIN 四种类型,覆盖 95% 的多表查询需求。
1. 四类 JOIN 速览
sql
-- 假设有两张表:
-- employees: id, name, dept_id
-- departments: id, dept_name
-- INNER JOIN:两表都有才返回(交集)
SELECT e.name, d.dept_name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.id;
-- LEFT JOIN:左表全保留,右表没有就 NULL
SELECT e.name, d.dept_name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.id;
-- RIGHT JOIN:右表全保留,左表没有就 NULL(很少用,LEFT 反过来就好)
SELECT e.name, d.dept_name
FROM employees e
RIGHT JOIN departments d ON e.dept_id = d.id;
-- FULL JOIN:两表都保留,没有就 NULL(PG/MySQL 8+ 支持,SQLite 不支持)
SELECT e.name, d.dept_name
FROM employees e
FULL JOIN departments d ON e.dept_id = d.id;
| JOIN 类型 | 左表有 | 右表有 | 结果 |
|---|---|---|---|
| INNER | ✅ | ✅ | ✓ 返回 |
| INNER | ❌ | ✅ | ✗ |
| INNER | ✅ | ❌ | ✗ |
| LEFT | ✅ | ❌ | ✓ 右表为 NULL |
| LEFT | ❌ | ✅ | ✗ |
| RIGHT | ❌ | ✅ | ✓ 左表为 NULL |
| FULL | ✅ | ❌ | ✓ 右表为 NULL |
| FULL | ❌ | ✅ | ✓ 左表为 NULL |
2. INNER JOIN --- 最常用
sql
-- 查员工及其所属部门名
SELECT
e.employee_id,
e.name,
d.department_name,
d.location
FROM employees e
INNER JOIN departments d ON e.department_id = d.department_id;
-- 三表 JOIN:员工 → 部门 → 项目
SELECT
e.name,
d.department_name,
p.project_name
FROM employees e
INNER JOIN departments d ON e.department_id = d.department_id
INNER JOIN projects p ON e.employee_id = p.lead_id;
ON vs USING
如果关联的列名相同,可以用 USING 简化:
sql
-- 列名相同(都是 department_id)
SELECT e.name, d.department_name
FROM employees e
INNER JOIN departments d USING (department_id);
-- 等价写法
SELECT e.name, d.department_name
FROM employees e
INNER JOIN departments d ON e.department_id = d.department_id;
⚠️
USING会把关联列合并为一个,ON则保留两个列。
3. LEFT JOIN --- 查"有/无"
sql
-- 查所有员工及其部门(没有部门的员工也显示)
SELECT e.name, d.department_name
FROM employees e
LEFT JOIN departments d ON e.department_id = d.department_id;
-- 技巧:找"没有部门"的员工(左表有,右表没有)
SELECT e.name
FROM employees e
LEFT JOIN departments d ON e.department_id = d.department_id
WHERE d.department_id IS NULL;
-- 找"没有员工"的部门
SELECT d.department_name
FROM departments d
LEFT JOIN employees e ON d.department_id = e.department_id
WHERE e.employee_id IS NULL;
LEFT JOIN ... IS NULL 是非常经典的"找缺失"模式,等价于 NOT EXISTS。
4. RIGHT JOIN --- 对称写法
sql
-- RIGHT JOIN 只是 LEFT 的反向,通常直接用 LEFT 就行
-- 下面两个等价:
SELECT e.name, d.department_name
FROM employees e
RIGHT JOIN departments d ON e.department_id = d.department_id;
-- 等价 LEFT 写法(推荐)
SELECT e.name, d.department_name
FROM departments d
LEFT JOIN employees e ON d.department_id = e.department_id;
💡 习惯: 只用 LEFT JOIN 就够了,把主表放左边,副表放右边。RIGHT JOIN 除了在特定自动生成的 SQL 中外很少主动使用。
5. FULL JOIN --- 全保留
sql
-- PostgreSQL / MySQL 8+
SELECT
COALESCE(e.name, '无员工') AS employee,
COALESCE(d.department_name, '无部门') AS department
FROM employees e
FULL JOIN departments d ON e.department_id = d.department_id;
-- SQLite 模拟 FULL JOIN(不支持)
SELECT e.name, d.department_name
FROM employees e
LEFT JOIN departments d ON e.department_id = d.department_id
UNION
SELECT e.name, d.department_name
FROM employees e
RIGHT JOIN departments d ON e.department_id = d.department_id;
6. 自连接 --- 同一张表的 JOIN
sql
-- 查员工及其上级
SELECT
e.name AS employee,
m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.employee_id;
-- 查同部门的同事对
SELECT
a.name AS emp1,
b.name AS emp2,
a.department_id
FROM employees a
INNER JOIN employees b
ON a.department_id = b.department_id
AND a.employee_id < b.employee_id; -- 去重:只保留 (A,B) 不保留 (B,A)
自连接的核心要点:必须用别名,否则无法区分。
7. 多表 JOIN 的顺序与性能
sql
-- 4 表 JOIN
SELECT
o.order_id,
c.customer_name,
e.name AS sales_rep,
p.product_name,
oi.quantity
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
JOIN employees e ON o.sales_rep_id = e.employee_id
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id;
性能要点
| 原则 | 说明 |
|---|---|
| 小表驱动大表 | JOIN 的顺序影响性能(优化器会重排,但建议手动优先小表) |
| 关联列建索引 | JOIN 的 ON 条件列必须有索引 |
| 避免 JOIN 太多表 | 超过 5 个 JOIN 可读性和性能都会变差,考虑反范式或视图 |
| 先过滤再 JOIN | 用子查询或 CTE 先缩小结果再 JOIN |
sql
-- 先过滤再 JOIN 的优化写法
SELECT e.name, d.department_name
FROM employees e
INNER JOIN (
SELECT department_id, department_name
FROM departments
WHERE status = 'active'
) d ON e.department_id = d.department_id
WHERE e.status = '在职';
8. 实战:多表查询完整场景
sql
-- 场景:生成 2024 年销售报表
SELECT
c.customer_name,
COUNT(o.order_id) AS order_count,
SUM(o.total_amount) AS total_spent,
MAX(o.order_date) AS last_order
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_date >= '2024-01-01'
AND o.status = '已完成'
GROUP BY c.customer_id, c.customer_name
HAVING COUNT(o.order_id) >= 3
ORDER BY total_spent DESC
LIMIT 10;
📝 要点总结
| JOIN 类型 | 场景 | 记忆口诀 |
|---|---|---|
| INNER JOIN | 两表都有才要 | 交集 |
| LEFT JOIN | 主表全保留 | 左表为王 |
| RIGHT JOIN | 副表全保留 | 尽量换 LEFT |
| FULL JOIN | 两表都保留 | 全集 |
| 自连接 | 同一张表内关联 | 必须用别名 |
ON |
关联条件 | 列名不同时用 |
USING |
关联列同名 | 列名相同时用 |
LEFT JOIN + IS NULL |
找缺失数据 | 经典"不存在"查询 |