码上问学问代码之理,学编程之道

第 5 章 · 多表连接与子查询

真实的数据从来不挤在一张表里:作品表只记了一个 category_id,可页面上要显示的是「唐诗」这个名字,那就得把 category 表请过来。本章讲两件本事——JOIN 把多张表拼成一张宽表,子查询在一个查询里套另一个查询。学完它,你就能把散落在「作品 / 分类 / 书评 / 用户」四张表里的信息,一句话查全。

为什么需要 JOIN

一张表装不下所有信息
先讲为什么你去看「古典文学网」的作品列表,每条显示的是「静夜思 / 李白 / 唐诗」。可数据库里的 article 表存的却是 category_id = 1——一个数字。为什么?因为「唐诗」这个名字只需要在 category 表里存一次,作品表存个编号指过去就够了。
这就像图书馆:每本书上贴的是「索书号」,而不是把整个书架的内容抄一遍。可用户要看的是书名对应的分类名,于是我们就必须把两张表「接」起来看——这就是 JOIN(连接)。
动手做先看看作品表里到底存了什么。你会发现它只有 category_id,没有分类名。
查询.sqlsql可运行
SELECT id, title, category_id FROM article;
查询结果SQL 执行结果
运行结果: id | title | category_id 1 | 静夜思 | 1 2 | 春望 | 1 3 | 念奴娇·赤壁怀古 | 2 4 | 声声慢 | 2 5 | 天净沙·秋思 | 3 6 | 陋室铭 | 4 -- 共 6 行
讲透
  • 为什么要拆表:分类名如果每条作品都存一遍,改了分类名就得改几百行,还会出现同一个分类拼写不一致。拆成两张表、用编号关联,改一处即可,且不会写错。
  • 「关联的那一列」就是桥梁:article.category_id 指向 category.id,这就是外键(第 7 章细讲)。JOIN 就是顺着这座桥把两张表拼宽。
  • JOIN 不会改数据:它只在「查询的那一刻」把两张表拼成一张临时宽表,表里原来的数据一动不动。

INNER JOIN:只留两边都有的

最常用的连接
三种 JOIN:交集、保左边、保右边——先想清楚「谁不能丢」再选INNER JOIN只要两边都有的LEFT JOIN左边全保留,右边没有补 NULLRIGHT JOIN右边全保留,左边没有补 NULL本项目最常用:作品列表 = article LEFT JOIN category(没分类的作品也要显示)INNER JOIN 只会留下「有分类的作品」——分类为空的行悄悄消失,数据对不上账
动手做把 article 和 category 用 ON 指定「谁等于谁」,作品的分类名就出来了。给两张表各起一个短别名(a、c),列名前带上别名更清楚。
连接查询.sqlsql可运行
SELECT a.id, a.title, a.author, c.name AS 分类
FROM article a
JOIN category c ON a.category_id = c.id
ORDER BY a.id;
查询结果SQL 执行结果
运行结果: id | title | author | 分类 1 | 静夜思 | 李白 | 唐诗 2 | 春望 | 杜甫 | 唐诗 3 | 念奴娇·赤壁怀古 | 苏轼 | 宋词 4 | 声声慢 | 李清照 | 宋词 5 | 天净沙·秋思 | 马致远 | 元曲 6 | 陋室铭 | 刘禹锡 | 散文 -- 共 6 行
讲透
  • JOIN 就是 INNER JOIN:两个写法完全等价。它的含义是「两边的记录必须都匹配得上,才出现在结果里」——左边这条作品的 category_id 在右边找不到对应分类,这条作品就被丢掉。
  • ON 后面写「匹配条件」:a.category_id = c.id。它不是筛选(WHERE),而是告诉数据库「这两张表的行怎么配对」。
  • 执行顺序:数据库先按 ON 把行配好对,再按 WHERE 筛,最后 SELECT 取列。所以 JOIN 之后可以照常用 WHERE、ORDER BY、LIMIT。
  • 本库 6 篇作品的 category_id 都填了值,都能匹配上分类,所以 INNER JOIN 恰好是 6 行。下一节你会看到,一旦有「配不上」的数据,结果就不一样了。
避坑两个新手错误:① 忘了写 ON 条件——FROM article a JOIN category c 会退化成「每篇作品 × 每个分类」的笛卡尔积(下一节详细说);② 列名有歧义——id、status、created_at 这些列两张表都有,只写 SELECT id 会报 Column 'id' in field list is ambiguous,务必写成 a.id 或 c.id。

LEFT / RIGHT JOIN:保底的那一边

没有书评的作品,也要显示
先讲为什么分类「小说」下面目前一本书都没有。如果用 INNER JOIN 查「分类 + 它下面的作品」,这个分类会被直接丢掉——可页面上分类导航栏必须把它列出来,只是作品数为 0 而已。
这时候你要的不是「都有」,而是「左边表一条都不能少」,这正是 LEFT JOIN(左连接)要解决的问题。
动手做以 category 为左表,左连接 article。左边 5 个分类会全部保留,「小说」下面没有作品,作品那一列就填 NULL。
左连接.sqlsql可运行
-- 以 category 为左表:5 个分类一条不少
SELECT c.name AS 分类, a.title AS 作品
FROM category c
LEFT JOIN article a ON a.category_id = c.id
ORDER BY c.id, a.id;
查询结果SQL 执行结果
运行结果: 分类 | 作品 唐诗 | 静夜思 唐诗 | 春望 宋词 | 念奴娇·赤壁怀古 宋词 | 声声慢 元曲 | 天净沙·秋思 散文 | 陋室铭 小说 | NULL -- 共 7 行

把两张表左右一交换,就得到了 RIGHT JOIN(右连接)——它保证「右边那张表一条不少」。下面这句写法和上面完全等价:

右连接.sqlsql可运行
-- 以 category 为右表:右边的 5 个分类一条不少
SELECT c.name AS 分类, a.title AS 作品
FROM article a
RIGHT JOIN category c ON a.category_id = c.id
ORDER BY c.id, a.id;
查询结果SQL 执行结果
运行结果: 分类 | 作品 唐诗 | 静夜思 唐诗 | 春望 宋词 | 念奴娇·赤壁怀古 宋词 | 声声慢 元曲 | 天净沙·秋思 散文 | 陋室铭 小说 | NULL -- 共 7 行
动手做再看开头那个需求:没有书评的作品也要显示。以 article 为左表左连接 comment,有书评的作品会出现多行,没书评的作品书评列为 NULL。
作品与书评.sqlsql可运行
-- 6 篇作品一条都不能少;没书评的作品,书评列为 NULL
SELECT a.id, a.title, cm.content AS 书评
FROM article a
LEFT JOIN comment cm ON cm.article_id = a.id
ORDER BY a.id, cm.id;
查询结果SQL 执行结果
运行结果: id | title | 书评 1 | 静夜思 | 短短二十字,写尽思乡之情。 1 | 静夜思 | 小时候背,只觉得朗朗上口;长大后读,才懂那份乡愁。 2 | 春望 | NULL 3 | 念奴娇·赤壁怀古 | 大江东去,三个字就起势了,果然是豪放派。 4 | 声声慢 | NULL 5 | 天净沙·秋思 | NULL 6 | 陋室铭 | NULL -- 共 7 行
讲透
  • LEFT JOIN 保证「左表一行不少」:左表的每一行都保留,右表配不上就用 NULL 补位。
  • RIGHT JOIN 和 LEFT JOIN 是镜面对称:A LEFT JOIN B 等价于 B RIGHT JOIN A。所以实践中统一用 LEFT JOIN 就够了,把「要全的那个」放左边,读起来最顺。
  • 「没书评的作品」这类需求就是 LEFT JOIN 的典型:主表一条不少,从表有就用真值、没有就 NULL。想进一步只挑出「确实没书评的」,再加 WHERE cm.id IS NULL(注意是判断主键/非空列,不要判断 cm.content)。
  • 怎么选:两张表都非有不可 → INNER JOIN;「主表不能少」→ LEFT JOIN。

自连接:评论的「楼中楼」

一张表自己连自己
自连接:comment 表自己连自己,pid 指向同表的 id——楼中楼就出来了comment 表(真实存储)id 101 · pid NULL为什么静夜思这么短?id 102 · pid 101因为即兴成诗,一气呵成id 103 · pid 101补充:传闻是酒后之作id 104 · pid NULL春望读着最沉重pid = 101 的行,都是 101 的回复页面展示(楼中楼)① 为什么静夜思这么短?楼层 1 · 主评论↳ 因为即兴成诗,一气呵成↳ 补充:传闻是酒后之作② 春望读着最沉重楼层 2 · 主评论SQL:SELECT c.*, r.* FROM comment c LEFT JOIN comment r ON r.pid = c.id;
先讲为什么书评下面还能再回复,回复下面还能再回复——这种「楼中楼」结构,真要为每一层建一张表就疯了。comment 表的做法很聪明:加一列 parent_id,如果这条是回复,就填「它回复的那条评论的 id」。
于是「回复」和「被回复」都在同一张表里。要同时看到两边的内容,就得把 comment 表连它自己——这叫自连接。
动手做种子里 3 条书评的 parent_id 全是 NULL(没有回复),所以先临时插一条回复,再自连接去查。给同一张表起两个别名:r 代表回复(reply),p 代表被回复的父评论(parent)。
自连接.sqlsql可运行
-- ① 种子里 3 条书评的 parent_id 都是 NULL,先插一条临时回复
INSERT INTO `comment` (`user_id`, `article_id`, `parent_id`, `content`)
VALUES (2, 1, 1, '同感,尤其最后一句。');

-- ② 自连接:把「回复」和它回复的那条评论配成一对
SELECT r.id AS 回复ID, r.content AS 回复,
       p.id AS 被回复ID, p.content AS 被回复
FROM `comment` r
JOIN `comment` p ON r.parent_id = p.id;
查询结果SQL 执行结果
运行结果: 回复ID | 回复 | 被回复ID | 被回复 4 | 同感,尤其最后一句。 | 1 | 短短二十字,写尽思乡之情。 -- 共 1 行
统计每条评论的回复数.sqlsql可运行
-- 仍然是自连接,这次用 LEFT JOIN:没有回复的评论也要显示为 0
SELECT p.id AS 评论ID, p.content AS 评论, COUNT(r.id) AS 回复数
FROM `comment` p
LEFT JOIN `comment` r ON r.parent_id = p.id
GROUP BY p.id, p.content
ORDER BY p.id;
查询结果SQL 执行结果
运行结果: 评论ID | 评论 | 回复数 1 | 短短二十字,写尽思乡之情。 | 1 2 | 小时候背,只觉得朗朗上口;长大后读,才懂那份乡愁。 | 0 3 | 大江东去,三个字就起势了,果然是豪放派。 | 0 4 | 同感,尤其最后一句。 | 0 -- 共 4 行
讲透
  • 自连接的关键是「起两个别名」:comment r JOIN comment p,数据库把它当成两张独立的表看待。不起别名就会报「表名重复」。
  • 连接条件和普通 JOIN 一模一样:ON r.parent_id = p.id——回复的父 id,指向父评论的 id。
  • 本节的临时数据:上面那条 INSERT 只是为了演示。看完请执行 DELETE FROM comment WHERE parent_id IS NOT NULL; 洗掉它,本章后面所有示例都按原始种子(3 条书评)来算。
  • 再扩一层就变三表连接:想知道「谁回复了谁、针对哪篇文章」,就再 JOIN 上 user 和 article——连接的表可以一直加下去。

CROSS JOIN 与笛卡尔积

为什么它很危险
先讲为什么CROSS JOIN(交叉连接)不加任何匹配条件,它做的是「左表的每一行,和右表的每一行,两两组合」。所以 5 个分类 × 2 个用户 = 10 行。这个「两两全配」的结果就叫笛卡尔积。
它偶尔有用(生成「所有组合」),但绝大多数时候是事故——你以为在连接,其实是在制造爆炸。
动手做把 category 和 user 交叉连接,看看「两两组合」长什么样。
交叉连接.sqlsql可运行
-- 5 个分类 × 2 个用户 = 10 行
SELECT c.name AS 分类, u.nickname AS 用户
FROM category c
CROSS JOIN user u
ORDER BY c.id, u.id;
查询结果SQL 执行结果
运行结果: 分类 | 用户 唐诗 | 管理员 唐诗 | 青莲居士 宋词 | 管理员 宋词 | 青莲居士 元曲 | 管理员 元曲 | 青莲居士 散文 | 管理员 散文 | 青莲居士 小说 | 管理员 小说 | 青莲居士 -- 共 10 行
避坑写 JOIN 时忘写 ON 条件,就等于写了一个 CROSS JOIN。两张各 1 万行的表,笛卡尔积就是 1 亿行——数据库会跑到内存爆掉、查询卡死。所以:写完 JOIN 先检查有没有 ON;组合结果异常地多,第一反应就是「连接条件是不是漏了」。只有当你的需求真的是「要所有组合」时,才主动用 CROSS JOIN。
讲透记住一句话:JOIN 的「配对条件」决定结果有多大。条件写得越精确,配对越少;条件漏了,结果就成倍膨胀。CROSS JOIN 也有正经用途,比如「给每个仓库 × 每个日期生成一张空的库存盘点表」,那时我们需要的就是全组合。

UNION:上下拼两个结果集

和 JOIN 不是一回事
先讲为什么JOIN 是「左右并排」拼列;但有时候你要的是「上下堆叠」拼行——比如「唐诗的作品」和「宋词的作品」合成一个列表,或者「作者表」和「用户表」里的人都放进一个「贡献者」清单。这类「两个结构相同的查询结果叠起来」的需求,用的是 UNION,不是 JOIN。
动手做把「唐诗分类的作品」和「唐朝的作品」叠起来。注意两者有重叠(静夜思、春望既属于唐诗分类、又是唐朝作品),正好用来对比 UNION 的去重效果。
UNION 去重.sqlsql可运行
-- UNION:合并后自动去掉重复行 → 3 行
SELECT title FROM article WHERE category_id = 1
UNION
SELECT title FROM article WHERE dynasty = '唐';
查询结果SQL 执行结果
运行结果: title 静夜思 春望 陋室铭 -- 共 3 行(静夜思、春望重复,被合并为一条)
UNION ALL 全部保留.sqlsql可运行
-- UNION ALL:不去重,两个结果原样叠起来 → 5 行
SELECT title FROM article WHERE category_id = 1
UNION ALL
SELECT title FROM article WHERE dynasty = '唐';
查询结果SQL 执行结果
运行结果: title 静夜思 春望 静夜思 春望 陋室铭 -- 共 5 行
讲透
  • UNION 去重、UNION ALL 不去重。去重要额外排序/比对,代价更高,所以确定没有重复、又不在意重复时,优先用 UNION ALL,更快。
  • 两个 SELECT 的列数必须相同、类型要能对应,最终列名取第一个 SELECT 的。
  • 结果默认无序:上面「静夜思、春望、陋室铭」只是其中一种可能顺序。要固定顺序,把 ORDER BY 写在整段 UNION 的最后,不能写在单个 SELECT 里。
  • UNION 是「拼行」,JOIN 是「拼列」——别搞混,这是最常被问到的区别。
避坑把 ORDER BY 写在 UNION 的前半段里,MySQL 会以为你想给第一个 SELECT 排序,报 Incorrect usage of UNION and ORDER BY。正确做法:先把两段 UNION 写完,最后统一 ORDER BY。另外,两段列数不一致会报 The used SELECT statements have a different number of columns。

子查询:查询里再套一个查询

IN / 标量 / EXISTS
子查询:先算里层,再把结果交给外层当条件外层查询SELECT title, views FROM article里层子查询(先执行)WHERE views > (SELECT AVG(views) FROM article)→ 算出全表平均浏览量:87.4WHERE views >一个数执行顺序:① 里层先跑,算出一个值/一列值 ② 外层拿它当条件再跑三种子查询返回:标量(一个数)→ =、> 比较;一列多行 → IN;存在性 → EXISTSIN (SELECT category_id FROM article) —— 拿到一列 id,去外层匹配经验:子查询能看懂优先;太深(超过两层)就拆成 JOIN 或临时结果
先讲为什么「查出有书评的作品」——你要的条件不在 article 表里,它藏在 comment 表。办法之一就是先算出「哪些作品 id 有书评」,再拿它去筛作品表。这种「把一个小查询的结果,当成另一个查询的条件」的写法,就是子查询。
它像一个「临时小抄」:先问一句、记下答案,再拿着答案问下一句。
动手做①IN 子查询:内层查出所有被评论过的作品 id,外层用它筛作品。
IN 子查询.sqlsql可运行
-- 种子里的 3 条书评分别评的是作品 1、1、3 → 内层结果只有 1 和 3
SELECT id, title
FROM article
WHERE id IN (SELECT article_id FROM comment)
ORDER BY id;
查询结果SQL 执行结果
运行结果: id | title 1 | 静夜思 3 | 念奴娇·赤壁怀古 -- 共 2 行
动手做②标量子查询:内层只返回一个值(这里是平均浏览量),外层拿它当普通数字用。
标量子查询.sqlsql可运行
-- 6 篇作品平均浏览量 = (328+256+412+298+187+203)/6 ≈ 280.67
SELECT title, views
FROM article
WHERE views > (SELECT AVG(views) FROM article)
ORDER BY views DESC;
查询结果SQL 执行结果
运行结果: title | views 念奴娇·赤壁怀古 | 412 静夜思 | 328 声声慢 | 298 -- 共 3 行
动手做③EXISTS:不关心拿到什么具体值,只问「存在不存在」。把内层和外层关联起来,效果与 IN 一致,但写法是「针对每一行,去问一次有没有」。
EXISTS.sqlsql可运行
-- 对每个作品,检查 comment 里有没有 article_id 等于它的记录
SELECT a.id, a.title
FROM article a
WHERE EXISTS (SELECT 1 FROM comment c WHERE c.article_id = a.id)
ORDER BY a.id;
查询结果SQL 执行结果
运行结果: id | title 1 | 静夜思 3 | 念奴娇·赤壁怀古 -- 共 2 行
动手做④标量子查询也能放在 SELECT 的列里,给每一行「现算」一个值——比如把评论人的昵称查出来。
列里的标量子查询.sqlsql可运行
SELECT c.id, c.content, 
       (SELECT u.nickname FROM user u WHERE u.id = c.user_id) AS 评论人
FROM comment c
ORDER BY c.id;
查询结果SQL 执行结果
运行结果: id | content | 评论人 1 | 短短二十字,写尽思乡之情。 | 管理员 2 | 小时候背,只觉得朗朗上口;长大后读,才懂那份乡愁。 | 青莲居士 3 | 大江东去,三个字就起势了,果然是豪放派。 | 管理员 -- 共 3 行
讲透
  • 子查询写在括号里,可以出现在 WHERE、FROM、SELECT 三个位置。
  • 返回多行多列 → 配 IN / EXISTS;返回一个值 → 当标量用(能和 >、= 直接比较)。返回多行却用 = 比较,会报 Subquery returns more than 1 row。
  • IN 和 EXISTS 怎么选:语义上「有书评的作品」两者等价;一般来说外层表大、内层表小,用 IN 更直观;外层表小,用 EXISTS 往往更快(因为它一找到匹配就停)。写作业时先求写对,性能交给第 8 章的 EXPLAIN。
  • 找「没有书评的作品」:把条件反过来,WHERE NOT EXISTS (SELECT 1 FROM comment c WHERE c.article_id = a.id),结果就是春望、声声慢、天净沙·秋思、陋室铭这 4 篇。
  • 子查询和 JOIN 常常能互相改写:同一件事,既能 JOIN 也能子查询。JOIN 更适合「要把另一张表的列显示出来」,子查询更适合「只用来做条件」。
避坑NOT IN 遇到内层结果里有 NULL 会「全军覆没」——整条查询返回 0 行,而且不报错,最难查。原因是 x NOT IN (…, NULL) 的结果既不是真也不是假,而是 NULL,行就被过滤掉了。所以「排除型」条件优先用 NOT EXISTS,或者在内层加 WHERE 列 IS NOT NULL。本库的 comment.article_id 是 NOT NULL 所以安全,但换成可空列就要当心。

ANY 与 ALL:和一组值比较

比「最小的那个」还是「最大的那个」
先讲为什么「比宋词里随便哪一篇都高」和「比宋词里所有篇都高」,是两句话、两个完全不同的结果。子查询返回的是一组值,而 > 只能跟一个值比——ANY 和 ALL 就是为「和一组值比大小」准备的。
动手做宋词两篇的浏览量是 412(念奴娇)和 298(声声慢)。先用 ANY(比其中最小的还高即可),再用 ALL(要超过最大的那个)。
ANY.sqlsql可运行
-- views > 298 即可(比宋词里「随便一篇」高就行)
SELECT title, views
FROM article
WHERE views > ANY (SELECT views FROM article WHERE dynasty = '宋')
ORDER BY views DESC;
查询结果SQL 执行结果
运行结果: title | views 念奴娇·赤壁怀古 | 412 静夜思 | 328 -- 共 2 行
ALL.sqlsql可运行
-- views >= 412 才行(要「比宋词里每一篇」都高)
SELECT title, views
FROM article
WHERE views >= ALL (SELECT views FROM article WHERE dynasty = '宋');

-- 反过来找「比宋词每一篇都低」的:
-- WHERE views < ALL (SELECT views FROM article WHERE dynasty = '宋')
查询结果SQL 执行结果
运行结果: title | views 念奴娇·赤壁怀古 | 412 -- 共 1 行
讲透
  • 一句话换算:> ANY(组) 相当于 > 组里的最小值;> ALL(组) 相当于 > 组里的最大值。< ANY / < ALL 同理,分别对应「小于最大值」「小于最小值」。
  • = ANY(组) 完全等价于 IN (组),两种写法随便挑。
  • 空集合的坑:如果子查询返回 0 行,ALL 会判定为「真」(返回全部行!),ANY 判定为「假」(返回 0 行)。所以用 ANY/ALL 时,最好先确认子查询不会为空。
  • 本库的 dynasty 有 3 个值(唐/宋/元),上面两组子查询都非空,结果稳定。

综合案例:作品列表页一条查全

把本章串起来

「古典文学网」的作品列表,每条要显示:标题 + 作者 + 分类名 + 书评数。这四样东西分散在 article、category、comment 三张表里。用本章学的 JOIN + 子查询,一句 SQL 就能查全。

作品列表.sqlsql可运行
-- 分类名用 JOIN 取,书评数用「列里的标量子查询」现算
SELECT a.id,
       a.title,
       c.name AS 分类,
       (SELECT COUNT(*) FROM comment cm WHERE cm.article_id = a.id) AS 书评数
FROM article a
LEFT JOIN category c ON a.category_id = c.id
ORDER BY a.id;
查询结果SQL 执行结果
运行结果: id | title | 分类 | 书评数 1 | 静夜思 | 唐诗 | 2 2 | 春望 | 唐诗 | 0 3 | 念奴娇·赤壁怀古 | 宋词 | 1 4 | 声声慢 | 宋词 | 0 5 | 天净沙·秋思 | 元曲 | 0 6 | 陋室铭 | 散文 | 0 -- 共 6 行
讲透:一条查询是怎么「拼」出来的① 主体数据在 article,先 FROM article a;② 分类名要跨表 → LEFT JOIN category(用 LEFT 是为了将来作品没分分类时也不丢);③ 书评数不在任何一行里,得「数一数」→ 用列里的标量子查询 COUNT(*);④ 最后排个序。你会发现:JOIN 负责「把横向的列凑齐」,子查询负责「算出纵向的汇总值」,两者配合,才是真实项目里查询的常态。
动手练习
1查出每篇作品的标题、作者和它所属的分类名(提示:article JOIN category),按作品 id 排序。
2列出所有分类,以及每个分类下的作品数;没有作品的分类(小说)也必须显示出来,作品数记为 0(提示:用 LEFT JOIN + GROUP BY)。
3迁移练习:「红影赏析」有 movie 表(id、title、genre_id、rating)和 genre 表(id、name)。请查出每部电影的标题和类型名;再查出「一个电影都没有的类型」也要显示出来。想一想:这跟「作品 + 分类」是不是同一件事?
参考答案
练习答案.sqlsql可运行
-- 第 1 题
SELECT a.title, a.author, c.name AS 分类
FROM article a
JOIN category c ON a.category_id = c.id
ORDER BY a.id;

-- 第 2 题:以分类为左表,没有作品的分类也要显示
SELECT c.name AS 分类, COUNT(a.id) AS 作品数
FROM category c
LEFT JOIN article a ON a.category_id = c.id
GROUP BY c.id, c.name
ORDER BY c.id;

-- 第 3 题(红影赏析,表结构不同,思路完全一致)
SELECT m.title, g.name AS 类型
FROM movie m
JOIN genre g ON m.genre_id = g.id
ORDER BY m.id;

SELECT g.name AS 类型, COUNT(m.id) AS 影片数
FROM genre g
LEFT JOIN movie m ON m.genre_id = g.id
GROUP BY g.id, g.name
ORDER BY g.id;
查询结果SQL 执行结果
运行结果: -- 第 1 题 title | author | 分类 静夜思 | 李白 | 唐诗 春望 | 杜甫 | 唐诗 念奴娇·赤壁怀古 | 苏轼 | 宋词 声声慢 | 李清照 | 宋词 天净沙·秋思 | 马致远 | 元曲 陋室铭 | 刘禹锡 | 散文 -- 共 6 行 -- 第 2 题 分类 | 作品数 唐诗 | 2 宋词 | 2 元曲 | 1 散文 | 1 小说 | 0 -- 共 5 行 -- 第 3 题(在红影的库里执行) title | 类型 流浪地球 | 科幻 -- 具体条数以库里数据为准;第二个查询会连「没有影片的类型」一起列出来

第 2、3 题的关键:「要全的那张表放左边,用 LEFT JOIN」;统计数量时用 COUNT(a.id)(数从表的主键)而不是 COUNT(*),否则连 NULL 那行也会被数成 1。

本章小结