SQL多表查询完全指南:JOIN的7种用法详解
数据分析师被问到最多的SQL问题,不是窗口函数,不是子查询,而是——
这两张表,怎么关联?
JOIN是数据分析工作中使用频率最高的操作之一。但很多人只会 LEFT JOIN,遇到复杂业务场景就懵了。
本文把JOIN的7种写法全部整理出来,每种附真实业务场景 + 完整代码注释,收藏备用。
一、准备工作:建立测试数据
本文用两张表演示所有JOIN类型:
-- 用户表
CREATE TABLE users (
user_id INT PRIMARY KEY,
user_name VARCHAR(50),
dept_id INT,
join_date DATE
);
-- 部门表
CREATE TABLE departments (
dept_id INT PRIMARY KEY,
dept_name VARCHAR(50),
manager_id INT
);
-- 插入测试数据
INSERT INTO users VALUES
(1, '张伟', 10, '2023-01-15'),
(2, '李娜', 20, '2023-03-22'),
(3, '王芳', 10, '2023-06-01'),
(4, '陈磊', 30, '2023-08-10'),
(5, '刘洋', NULL, '2023-09-20'); -- 未分配部门
INSERT INTO departments VALUES
(10, '技术部', 1),
(20, '产品部', 2),
(40, '运营部', NULL); -- 运营部暂无用户
数据说明:
📊 users表有5条记录,其中刘洋(user_id=5)未分配部门(dept_id=NULL)
📊 departments表有3条记录,其中运营部(dept_id=40)没有对应用户
📊 这种"边界情况"正是各种JOIN产生差异的核心
二、INNER JOIN(内连接):最常用的精确匹配
**作用:**只返回两表中都存在匹配关系的行。
-- 查询所有已分配部门的用户信息
SELECT
u.user_id,
u.user_name,
d.dept_name,
u.join_date
FROM users u
INNER JOIN departments d
ON u.dept_id = d.dept_id;
-- 结果:返回4条记录(刘洋因dept_id=NULL不参与匹配,被排除)
-- user_id | user_name | dept_name | join_date
-- 1 | 张伟 | 技术部 | 2023-01-15
-- 2 | 李娜 | 产品部 | 2023-03-22
-- 3 | 王芳 | 技术部 | 2023-06-01
-- 4 | 陈磊 | 财务部 | 2023-08-10
**适用场景:**需要精确匹配数据时,比如查询"有订单的用户"、"有课程的学生"。
**注意:**INNER JOIN 可以简写为 JOIN,效果相同。
三、LEFT JOIN(左连接):保留左表全部数据
**作用:**返回左表所有行,右表没有匹配则填NULL。
-- 查询所有用户,包括未分配部门的
SELECT
u.user_id,
u.user_name,
COALESCE(d.dept_name, '未分配') AS dept_name, -- NULL替换为"未分配"
u.join_date
FROM users u
LEFT JOIN departments d
ON u.dept_id = d.dept_id;
-- 结果:返回5条记录(刘洋的dept_name显示为"未分配")
-- user_id | user_name | dept_name | join_date
-- 1 | 张伟 | 技术部 | 2023-01-15
-- 2 | 李娜 | 产品部 | 2023-03-22
-- 3 | 王芳 | 技术部 | 2023-06-01
-- 4 | 陈磊 | 财务部 | 2023-08-10
-- 5 | 刘洋 | 未分配 | 2023-09-20
**适用场景:**数据盘点、用户全量分析。比如"所有用户的下单情况"(包括从未下单的用户)。
**高频用法:**LEFT JOIN + WHERE 右表字段 IS NULL,等价于"在左表但不在右表"的差集查询。
-- 找出未分配部门的用户(LEFT JOIN反向查找)
SELECT u.user_id, u.user_name
FROM users u
LEFT JOIN departments d ON u.dept_id = d.dept_id
WHERE d.dept_id IS NULL;
-- 结果:只返回刘洋(dept_id=NULL,无法匹配到部门)
四、RIGHT JOIN(右连接):保留右表全部数据
**作用:**与LEFT JOIN相反,返回右表所有行,左表没有匹配则填NULL。
-- 查询所有部门,包括没有员工的部门
SELECT
COALESCE(u.user_name, '暂无员工') AS user_name,
d.dept_id,
d.dept_name
FROM users u
RIGHT JOIN departments d
ON u.dept_id = d.dept_id;
-- 结果:运营部(dept_id=40)也会出现,user_name显示"暂无员工"
**实践建议:**RIGHT JOIN 实际使用较少,因为把表顺序交换后用LEFT JOIN可以得到相同结果,代码更易读。建议统一用LEFT JOIN。
五、FULL OUTER JOIN(全外连接):两表数据全保留
**作用:**返回左表和右表的所有行,不匹配的一侧填NULL。
-- MySQL不支持FULL OUTER JOIN,用UNION模拟
SELECT
u.user_id, u.user_name,
d.dept_id, d.dept_name
FROM users u
LEFT JOIN departments d ON u.dept_id = d.dept_id
UNION
SELECT
u.user_id, u.user_name,
d.dept_id, d.dept_name
FROM users u
RIGHT JOIN departments d ON u.dept_id = d.dept_id;
-- 注:PostgreSQL / SQL Server / Oracle 直接支持 FULL OUTER JOIN
**适用场景:**数据对账、两个数据源合并,需要看"哪边有、哪边没有"的完整对比。
六、CROSS JOIN(交叉连接):笛卡尔积
**作用:**返回两表的笛卡尔积,左表每行与右表每行都组合一次。
-- 生成所有用户×所有部门的组合(通常用于报表日期×维度展开)
SELECT
u.user_name,
d.dept_name
FROM users u
CROSS JOIN departments d;
-- 结果:5 × 3 = 15 条记录(每个用户对应每个部门各一条)
-- 实际业务用法:生成日期×产品的所有组合,填充缺失日期
SELECT
date_list.dt,
product_list.product_id
FROM (SELECT DISTINCT order_date AS dt FROM orders) date_list
CROSS JOIN (SELECT DISTINCT product_id FROM products) product_list;
**注意:**CROSS JOIN结果集 = 两表行数之积,数据量大时容易撑爆内存,谨慎使用。
七、SELF JOIN(自连接):同一张表JOIN自身
**作用:**表和自身做关联,常用于层级数据、比较同表不同行。
-- 查询每个用户的上级经理名称(同一张users表)
SELECT
u.user_id,
u.user_name AS employee_name,
manager.user_name AS manager_name
FROM users u
LEFT JOIN users manager
ON u.manager_id = manager.user_id; -- 用户的manager_id关联同表的user_id
-- 找出同一部门入职时间最接近的"前一个人"
SELECT
a.user_name,
a.dept_id,
a.join_date,
MAX(b.join_date) AS prev_join_date
FROM users a
LEFT JOIN users b
ON a.dept_id = b.dept_id -- 同部门
AND b.join_date < a.join_date -- 比当前人早入职
GROUP BY a.user_id, a.user_name, a.dept_id, a.join_date;
**适用场景:**组织架构查询、时间序列比较、同维度排名对比。
八、多表JOIN:三张以上表的关联
-- 三表关联:用户 + 部门 + 订单
SELECT
u.user_name,
d.dept_name,
COUNT(o.order_id) AS order_count,
SUM(o.order_amount) AS total_amount
FROM users u
LEFT JOIN departments d ON u.dept_id = d.dept_id
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY u.user_id, u.user_name, d.dept_name
ORDER BY total_amount DESC;
多表JOIN注意事项:
① 关联字段要有索引,否则大表JOIN性能极差
② 先过滤再JOIN,用子查询或CTE减少参与关联的行数
③ 注意重复数据:一对多关系JOIN后行数会膨胀,GROUP BY前先确认逻辑
九、七种JOIN速查表
JOIN类型 | 结果集 | 适用场景
INNER JOIN | 两表都有匹配的行 | 精确关联,去除无匹配数据
LEFT JOIN | 左表全部 + 右表匹配 | 保留主表全量,查关联维度
RIGHT JOIN | 右表全部 + 左表匹配 | 同LEFT JOIN,建议换成LEFT JOIN
FULL OUTER JOIN | 两表全部行 | 数据对账,发现两边的差异
CROSS JOIN | 笛卡尔积 | 日期/维度展开,生成所有组合
SELF JOIN | 同表自关联 | 层级结构,同表行间比较
LEFT JOIN反向 | 只在左表不在右表的行 | 找差集,发现缺失数据
十、船长说
SQL里JOIN写错,数据就错了——而且往往不报错,只是静默地给你错误结果。
三个习惯可以帮你避坑:
① 写完JOIN先SELECT COUNT(*),看行数是否符合预期
② 多表JOIN先两两验证,再逐步加第三张表
③ 遇到NULL就用COALESCE或IS NULL显式处理,别让NULL传播
本文代码均在MySQL 8.0 / PostgreSQL 15 验证通过。
关注船长Talk公众号,回复「SQL」获取SQL数据分析完整系列文章。