欢迎光临
我们一直在努力

零基础学SQL 17:一个报表要查 4 张表?多表查询就这 5 步(附万能流程模板)

📝 实战数据:和前面 16 篇共用一套四表数据(员工表/订单表/用户表/任务表),文末给出完整 DROP+CREATE+INSERT,复制即可跑

🔗 系列目录:零基础学SQL:从入门到精通完整目录 📌 上篇回顾:篇16 自连接(一张表当两张表用) ⏱️ 阅读时长:约 10 分钟 🖼️ 本文含 3 张原理图(主表挂载 / 5 步流程 / 结果对比),发布时按文末"配图清单"上传

一、先搞懂:多表查询的本质是"定主表 + 往上面挂"

先看一个真实需求——老板要一张报表:

每个部门的员工数、订单总额、已付款订单数。

这个需求要跨两张表才能算出来:员工信息在员工表,订单信息在订单表。很多人一上来就写 FROM 员工表, 订单表 WHERE …,写完发现数字对不上,也不知道错在哪。

根本原因是没想清楚一件事:我这张报表的"一行"代表什么?

答案是"一行 = 一个部门"。一旦确定了这件事,思路立刻清晰:

  • 主表 = 能唯一确定"一行"的那张表 → 部门信息来自员工表,所以员工表是主表
  • 挂表 = 其余要的数据,一张一张往主表上 LEFT JOIN → 订单表挂上去 在这里插入图片描述

💡 上图:员工表是中间那张主表,订单表、任务表、用户表都通过关联字段"挂"到它身上。多表查询不是把表堆在一起,而是先立住主表,再一张张挂。

关键词记一个:多表查询 = 先定主表(决定行数),再挂副表(补充列)。


二、多表查询 5 步(任何需求都能套)

在这里插入图片描述

这套流程不只适用于本文的例子,换任何需求、换任何表都能套:

第 1 步:定主表 —— 结果的"一行"代表什么?

先回答这个问题,再动手写 SQL。

需求结果的一行主表
每个部门的订单情况 一个部门 员工表(部门在这里)
每个员工的订单数 一个员工 员工表
每个订单的详情 一个订单 订单表
每个城市的用户数 一个城市 用户表

判断方法:要按什么分组(GROUP BY 什么),那个字段所在的表就是主表。

第 2 步:明确粒度 —— 一行到底算什么

“一行 = 一个部门” 意味着:技术部只出一行。但员工表里技术部有张三、李四两个人 —— JOIN 完之后技术部会变成多行,最后必须靠 GROUP BY 压回一行。

这一步不想清楚,第 4 步的 COUNT 一定会错(详见坑 1)。

第 3 步:逐个挂表 —— 用 LEFT JOIN

FROM 员工表 e
LEFT JOIN 订单表 o ON e.员工id = o.员工id

  • 用 LEFT JOIN 而不是 INNER JOIN,是为了保留主表的全部行 —— 一个订单都没有的员工(赵六)也必须出现在报表里,只是订单相关列是 NULL
  • 关联条件写在 ON 后面,筛选条件不要写在 WHERE(详见坑 2)
  • 列名前面加表别名(e.员工id),多表同名列能直接避免歧义(详见坑 3)

第 4 步:聚合 —— 想清楚"数的是行还是人"

这是最容易出错的一步。JOIN 之后行数会变多,直接 COUNT(*) 数出来的是行数,不是人数。

正确写法:

SELECT
e.部门,
COUNT(DISTINCT e.员工id) AS 员工数,
COALESCE(SUM(o.订单金额), 0) AS 订单总额,
SUM(CASE WHEN o.状态 = '已付款' THEN 1 ELSE 0 END) AS 已付款订单数
FROM 员工表 e
LEFT JOIN 订单表 o ON e.员工id = o.员工id
GROUP BY e.部门;

部门员工数订单总额已付款订单数
技术部 2 7000 2
销售部 2 4000 1

三个细节:

  • COUNT(DISTINCT e.员工id) —— 去重后才是真正的"人数"
  • COALESCE(…, 0) —— 没订单的部门会显示 NULL,用 COALESCE 转成 0,报表更好看
  • SUM(CASE WHEN …) —— 篇15 讲的 CASE WHEN 用在这里做条件计数,非常实用

第 5 步:校验 —— 三个自检动作

写完别急着交,花 30 秒做这三件事:

  • 行数对不对:结果应该是 2 行(2 个部门),如果出来 4 行,说明 GROUP BY 写漏了
  • 挑一行手算:技术部有张三(订单 3000+2500)和李四(订单 1500),总额 7000 ✓
  • NULL 检查:有没有该出现却消失的行?(赵六这类"零订单"的人最容易被 WHERE 误杀)

  • 三、实战:一个需求串起 4 张表

    把需求升级成真正的报表:

    每个员工的姓名、部门、所在城市、订单数、订单总额、任务数。

    这次要用到全部 4 张表:员工表(姓名部门)、订单表(订单)、任务表(任务)、用户表(城市)。

    先看看直接写会出什么问题

    — 看起来没问题,实际有坑
    SELECT e.姓名,
    COUNT(o.订单id) AS 订单数,
    COUNT(t.任务id) AS 任务数
    FROM 员工表 e
    LEFT JOIN 订单表 o ON e.员工id = o.员工id
    LEFT JOIN 任务表 t ON e.员工id = t.员工id
    GROUP BY e.姓名;

    姓名订单数任务数
    张三 2 2 ❌
    李四 1 1
    王五 1 1
    赵六 0 1

    张三只有 1 个任务(任务1),为什么数出来 2? 这就是坑 4,后面详解。

    正确写法:先把副表各自聚合好,再挂上去

    SELECT
    e.姓名,
    e.部门,
    u.地址,
    COALESCE(o.订单数, 0) AS 订单数,
    COALESCE(o.订单总额, 0) AS 订单总额,
    COALESCE(t.任务数, 0) AS 任务数
    FROM 员工表 e
    LEFT JOIN (SELECT 员工id, COUNT(*) AS 订单数, SUM(订单金额) AS 订单总额
    FROM 订单表 GROUP BY 员工id) o ON e.员工id = o.员工id
    LEFT JOIN (SELECT 员工id, COUNT(*) AS 任务数
    FROM 任务表 GROUP BY 员工id) t ON e.员工id = t.员工id
    LEFT JOIN 用户表 u ON e.姓名 = u.姓名
    ORDER BY e.员工id;

    姓名部门地址订单数订单总额任务数
    张三 技术部 邯郸市 2 5500 1
    李四 技术部 北京市 1 1500 1
    王五 销售部 邯郸市 1 4000 1
    赵六 销售部 NULL 0 0 1

    注意最后一行:赵六的地址是 NULL —— 因为用户表里只有张三、李四、王五三个人。这是 LEFT JOIN 的正常表现:主表行一定保留,副表没匹配上就填 NULL。

    💡 实际项目里员工表和用户表应该用 用户id 关联,而不是姓名(重名就串了)。这里为了演示"4 张表怎么用同一套示例数据串起来",才用姓名关联,你自己的项目千万别这么写。


    四、4 个坑(2 个报错 + 2 个不报错但结果错)

    坑 1:COUNT 数出来是"行数"不是"人数"(不报错,但结果错)

    刚 JOIN 完,员工表 ⊕ 订单表长这样(5 行,不是 4 行):

    姓名部门订单id订单金额状态
    张三 技术部 101 3000 已付款
    张三 技术部 102 2500 已付款
    李四 技术部 104 1500 待付款
    王五 销售部 103 4000 已付款
    赵六 销售部 NULL NULL NULL

    这时候直接数:

    SELECT e.部门, COUNT(e.员工id) AS 员工数, SUM(o.订单金额) AS 订单总额
    FROM 员工表 e LEFT JOIN 订单表 o ON e.员工id = o.员工id
    GROUP BY e.部门;

    部门员工数订单总额
    技术部 3 ❌ 7000
    销售部 2 ✅ 4000

    技术部明明只有 2 个人,数出来 3 —— 因为张三有 2 个订单,占了 2 行。

    最阴险的地方在于:销售部恰好数对了。这种"有的对有的错"最难排查,你会以为自己写法没问题,直到有人质疑"这个部门人数怎么比花名册多"。

    修法:数人用 COUNT(DISTINCT e.员工id)。

    坑 2:LEFT JOIN 写了,人还是消失了(不报错,但结果少行)

    想查"所有人 + 他们已付款的订单",很多人这么写:

    — 错误写法
    SELECT e.姓名, o.订单id, o.订单金额
    FROM 员工表 e LEFT JOIN 订单表 o ON e.员工id = o.员工id
    WHERE o.状态 = '已付款';

    结果只有 3 行:

    姓名订单id订单金额
    张三 101 3000
    张三 102 2500
    王五 103 4000

    李四和赵六不见了。原因:LEFT JOIN 给他们补的 o.状态 是 NULL,而 NULL = '已付款' 的结果是"未知",直接被 WHERE 过滤掉 —— LEFT JOIN 被你写成了 INNER JOIN。 在这里插入图片描述

    修法:把筛选条件写进 ON,不要写进 WHERE:

    — 正确写法
    SELECT e.姓名, o.订单id, o.订单金额
    FROM 员工表 e LEFT JOIN 订单表 o ON e.员工id = o.员工id AND o.状态 = '已付款';

    结果 5 行,李四、赵六回来了(订单列是 NULL):

    姓名订单id订单金额
    张三 101 3000
    张三 102 2500
    李四 NULL NULL
    王五 103 4000
    赵六 NULL NULL

    一句话记忆:想筛副表 → 条件写 ON;想筛最终结果 → 条件写 WHERE。

    坑 3:多表有同名列,SELECT 直接报错(MySQL 1052)

    员工表和订单表都有 员工id 这一列,不写别名就会报错:

    SELECT 员工id FROM 员工表 e JOIN 订单表 o ON e.员工id = o.员工id;

    ERROR 1052 (23000): Column '员工id' in field list is ambiguous

    修法:所有列名前面都加表别名(e.员工id、o.订单id)。

    建议养成习惯:只要出现 2 张及以上表,每一列都加别名前缀。多打几个字符,省掉一堆 1052 报错。

    坑 4:两张"一对多"表直接 JOIN,数字翻倍(不报错,但结果错)

    回到第三节那个例子:张三只做了 1 个任务,却数出 2 个。

    原因是行数相乘:张三有 2 个订单、1 个任务,JOIN 之后是 2 × 1 = 2 行,COUNT(t.任务id) 把同 1 个任务数了 2 遍。如果他有 3 个订单 2 个任务,就会变成 6 行,任务数被数成 6 —— 数据越干净越不容易发现,数据一多就离谱。

    两种修法,按场景选:

    — 修法 A(简单):加 DISTINCT,适合只数一种东西
    COUNT(DISTINCT t.任务id) AS 任务数

    — 修法 B(推荐):先把副表各自聚合好再 JOIN,适合要同时算多种指标
    LEFT JOIN (SELECT 员工id, COUNT(*) AS 任务数 FROM 任务表 GROUP BY 员工id) t
    ON e.员工id = t.员工id

    修法 B 也叫预聚合,它还有个额外好处:JOIN 之前行数就被压到最小,查询快得多。数据量上万行以后,这个差别非常明显。


    五、多表查询万能流程模板(换需求直接套,建议收藏)

    SELECT
    主表别名.分组字段,
    COUNT(DISTINCT 主表别名.主键) AS 数量, — 数"多少个",用 DISTINCT 防翻倍
    COALESCE(SUM(副表别名.金额字段), 0) AS 总额, — 没匹配上显示 0,不显示 NULL
    SUM(CASE WHEN 副表别名.状态 = '目标值' THEN 1 ELSE 0 END) AS 目标数量
    FROM 主表 主表别名
    LEFT JOIN (
    SELECT 关联字段, COUNT(*) AS 副表数量
    FROM 副表 GROUP BY 关联字段 — 一对多时先预聚合,防行数膨胀
    ) 副表别名 ON 主表别名.关联字段 = 副表别名.关联字段
    GROUP BY 主表别名.分组字段
    ORDER BY 总额 DESC;

    套用时的 4 个填空位:

    位置填什么
    主表 结果的"一行"代表什么,那个字段在哪张表
    分组字段 按什么汇总(部门 / 员工 / 城市 / 日期)
    副表 要补充的数据在哪张表,一对多就先在子查询里 GROUP BY
    关联字段 两张表靠哪一列对上(通常是 id)

    写完之后按第 5 步的三件事自检:行数对不对、挑一行手算、有没有该出现却消失的行。


    六、课后练习(先自己写,参考答案见文末)

  • 统计每个部门的已付款订单总额(没有订单的部门显示 0)
  • 查出下过单的员工姓名和订单数(一个订单都没有的员工不要出现)
  • 统计每个部门的任务数(提示:任务表和员工表是一对多,注意别翻倍)

  • 七、参考答案

    练习 1:

    SELECT e.部门,
    COALESCE(SUM(CASE WHEN o.状态 = '已付款' THEN o.订单金额 ELSE 0 END), 0) AS 已付款总额
    FROM 员工表 e LEFT JOIN 订单表 o ON e.员工id = o.员工id
    GROUP BY e.部门;

    部门已付款总额
    技术部 5500
    销售部 4000

    技术部是 5500 —— 张三的 3000 + 2500,李四那单是"待付款",不计入。

    练习 2:

    SELECT e.姓名, COUNT(o.订单id) AS 订单数
    FROM 员工表 e INNER JOIN 订单表 o ON e.员工id = o.员工id
    GROUP BY e.姓名;

    姓名订单数
    张三 2
    李四 1
    王五 1

    这里用 INNER JOIN(只要下过单的人),所以赵六不会出现 —— 这正是 INNER JOIN 的用途。

    练习 3:

    SELECT e.部门, COALESCE(SUM(t.任务数), 0) AS 任务数
    FROM 员工表 e
    LEFT JOIN (SELECT 员工id, COUNT(*) AS 任务数 FROM 任务表 GROUP BY 员工id) t
    ON e.员工id = t.员工id
    GROUP BY e.部门;

    部门任务数
    技术部 2
    销售部 2

    每个员工各有 1 个任务,两个部门各 2 人,所以都是 2。如果直接 JOIN 不预聚合,任务数不会错(因为每人只有 1 个任务),但数据量一大就会翻车 —— 养成预聚合的习惯更稳。


    八、下篇预告

    篇19:窗口函数进阶(PARTITION BY 与 ORDER BY 深入) —— 篇18 讲了 ROW_NUMBER / RANK / DENSE_RANK 的基本用法,篇19 会把"分组内排序"这件事彻底讲透:每组取前 3 名、组内累计排名、去重取最新一条,都是面试和实战的高频题。

    说明:篇18(窗口函数实战)已在系列化之前发布,建议先按目录顺序学完篇01-17,再看篇18、篇19。


    附:本文用到的测试数据(复制即可运行,可重复执行)

    — 员工表(沿用篇16,含上级id 列)
    DROP TABLE IF EXISTS 员工表;
    CREATE TABLE 员工表 (
    员工id INT PRIMARY KEY,
    姓名 VARCHAR(20),
    部门 VARCHAR(20),
    工资 DECIMAL(10,2),
    邮箱 VARCHAR(50),
    手机 VARCHAR(20),
    入职日期 DATE,
    上级id INT
    );
    INSERT INTO 员工表 VALUES
    (1,'张三','技术部',9000,'zhangsan@yh.com','13800000001','2019-03-01',NULL),
    (2,'李四','技术部',9000,'lisi@yh.com', '13800000002','2020-07-15',1),
    (3,'王五','销售部',9200,'wangwu@yh.com', '13800000003','2018-01-10',NULL),
    (4,'赵六','销售部',9200,'zhaoliu@yh.com', '13800000004','2021-05-20',3);

    — 订单表
    DROP TABLE IF EXISTS 订单表;
    CREATE TABLE 订单表 (
    订单id INT PRIMARY KEY,
    员工id INT,
    订单金额 DECIMAL(10,2),
    下单时间 DATETIME,
    付款时间 DATETIME,
    状态 VARCHAR(20)
    );
    INSERT INTO 订单表 VALUES
    (101, 1, 3000, '2026-09-01 10:00:00', '2026-09-01 10:30:00', '已付款'),
    (102, 1, 2500, '2026-09-02 11:00:00', '2026-09-02 11:20:00', '已付款'),
    (103, 3, 4000, '2026-09-03 14:00:00', '2026-09-03 14:30:00', '已付款'),
    (104, 2, 1500, '2026-09-04 09:00:00', NULL, '待付款');

    — 用户表
    DROP TABLE IF EXISTS 用户表;
    CREATE TABLE 用户表 (
    用户id INT PRIMARY KEY,
    姓名 VARCHAR(20),
    手机 VARCHAR(20),
    邮箱 VARCHAR(50),
    地址 VARCHAR(100)
    );
    INSERT INTO 用户表 VALUES
    (1,'张三','13900000001','zhangsan@yh.com','邯郸市'),
    (2,'李四','13900000002','lisi@yh.com', '北京市'),
    (3,'王五','13900000003','wangwu@yh.com', '邯郸市');

    — 任务表
    DROP TABLE IF EXISTS 任务表;
    CREATE TABLE 任务表 (
    任务id INT PRIMARY KEY,
    员工id INT,
    备注 VARCHAR(100),
    状态 VARCHAR(20)
    );
    INSERT INTO 任务表 VALUES
    (1, 1, '完成需求评审', '已完成'),
    (2, 2, NULL, '进行中'),
    (3, 3, '修复线上bug', '已完成'),
    (4, 4, NULL, '待分配');

    赞(0)
    未经允许不得转载:171主机测评 » 零基础学SQL 17:一个报表要查 4 张表?多表查询就这 5 步(附万能流程模板)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址