SQL多表查询完全指南:JOIN的7种用法详解(附完整代码注释)

0 阅读6分钟

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数据分析完整系列文章。