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

第 4 章 · 聚合函数与分组

前面两章的查询,返回的都是一行行「明细」。可后台那些数字——「全站一共几篇作品」「唐诗平均浏览量多少」——要的不是明细,是把很多行压成一个数。这一章教你用聚合函数把数据「加起来看」:COUNT 数数、SUM 求和、AVG 求平均、MIN/MAX 找极值,再用 GROUP BY 按朝代、按分类分别统计,最后用 HAVING 把「篇数太少」的组筛掉。学完这章,数据看板你就自己能做了。

为什么需要聚合

从「列出明细」到「给我答案」
先讲为什么你在后台首页想看到什么?是「全站一共 6 篇作品」这个数字,不是「静夜思、春望、念奴娇……」这一长串清单。
这两件事的性质完全不同:前者是把很多行压成一个结果,后者是原样返回每一行。上一章的 SELECT 只会做后者。你需要一类新工具——聚合函数。名字里的「聚合」就是「把一堆东西汇成一个」的意思。
动手做把两种问法摆在一起对比,你就明白聚合的价值了。
对比.sqlsql可运行
-- 不用聚合:把 6 行明细全给你,想看总数还得自己数
SELECT id, title FROM article;

-- 用聚合:直接给答案
SELECT COUNT(*) AS 作品总数 FROM article;
查询结果SQL 执行结果
运行结果: -- 第一条:只给数据,不给答案 id | title 1 | 静夜思 2 | 春望 3 | 念奴娇·赤壁怀古 4 | 声声慢 5 | 天净沙·秋思 6 | 陋室铭 -- 共 6 行 -- 第二条:一行就是答案 作品总数 6 -- 共 1 行
讲透
  • 聚合函数的特点:多行进去,一个值出来。不管表里有 6 行还是 600 万行,COUNT(*) 都只回你一个数字。
  • 为什么必须在数据库里算:如果把 600 万行取到 Python 里再数,网络传输和内存就先扛不住了。算在最靠近数据的地方,只把结果拿回来,这是最基本的性能常识。
  • 本课要用到的五个:COUNT 数个数、SUM 求和、AVG 求平均、MIN 最小、MAX 最大。它们统称「聚合函数」。
  • 落到项目里:后台看板每一个数字卡片(总作品数、总浏览量、平均点赞数),背后都是一条带聚合函数的 SQL。

COUNT:数一数有几条

COUNT(*) 与 COUNT(列) 不一样
先讲为什么「有几条」是最高频的问题,但问法其实有两种:「这个表里有多少行?」和「有多少行的这一列填了值?」
第二种才是业务真正关心的——比如「有多少条书评是回复」「有多少用户填了邮箱」。这两种问法在 SQL 里对应两个不同写法,用错了数字就是错的。
动手做(1)看一个「数出来不一样」的例子书评表里,parent_id 只有「回复」才有值。数一数:3 条书评里,有几条是回复?
COUNT-列-vs-星号.sqlsql可运行
SELECT COUNT(*)         AS 全部书评,
       COUNT(parent_id) AS 是回复的书评
FROM comment;
查询结果SQL 执行结果
运行结果: 全部书评 | 是回复的书评 3 | 0 -- 共 1 行 -- COUNT(*) 数的是「行数」= 3 -- COUNT(parent_id) 数的是「这一列有值的行数」;3 行都是 NULL,所以是 0
讲透:COUNT(*) 和 COUNT(列) 的差别
  • COUNT(*) 数「有多少行」,不管列里有没有值,NULL 也照样算一行。
  • COUNT(列名) 数「这一列有多少个非 NULL 值」,NULL 会被跳过。这是它俩唯一也是最重要的区别。
  • 想数「满足条件的行」,就用 WHERE 先筛,再 COUNT(*)——例如 SELECT COUNT(*) FROM comment WHERE parent_id IS NOT NULL,结果同样是 0,但语义更直白。
  • COUNT(1) 和 COUNT(*) 在 MySQL 里效果一样,写哪个都行,本课统一写 COUNT(*)。
动手做(2)用它做个「数据完整性检查」两个数一减,就能发现「有多少条数据缺了这一列」。对作品表来说,我们期望每篇作品都有分类。
数据体检.sqlsql可运行
SELECT COUNT(*)                 AS 作品总数,
       COUNT(category_id)       AS 有分类的作品,
       COUNT(*) - COUNT(category_id) AS 缺分类的作品
FROM article;
查询结果SQL 执行结果
运行结果: 作品总数 | 有分类的作品 | 缺分类的作品 6 | 6 | 0 -- 共 1 行 -- 相减为 0,说明 6 篇作品的 category_id 都填了值
避坑别用 COUNT(列) 去数「一共多少条记录」。只要那一列允许为 NULL,数字就会偏小。经典的翻车现场是统计用户数:写成 SELECT COUNT(email) FROM user,那些没填邮箱的用户就被漏掉了。定「总行数」一律用 COUNT(*);只有当你明确要「这一列填了值的行数」时,才写 COUNT(列名)。

SUM / AVG / MIN / MAX

求和、平均、极值
先讲为什么会数数之后,接下来要的是「加起来多少」和「平均多少」——全站总浏览量、平均点赞数、最热和最冷的那一篇。
这四个函数长得就像它们的英文意思:SUM 求和、AVG 平均(average 的缩写)、MIN 最小、MAX 最大。用起来几乎不用记语法。
动手做(1)四个函数一起上,给全站作品做个总览
总览.sqlsql可运行
SELECT SUM(views)              AS 总浏览量,
       ROUND(AVG(views), 1)    AS 平均浏览量,
       MIN(views)              AS 最少浏览量,
       MAX(views)              AS 最多浏览量
FROM article;
查询结果SQL 执行结果
运行结果: 总浏览量 | 平均浏览量 | 最少浏览量 | 最多浏览量 1684 | 280.7 | 187 | 412 -- 共 1 行 -- 6 篇相加:328 + 256 + 412 + 298 + 187 + 203 = 1684 -- 1684 ÷ 6 = 280.6666…,ROUND 后取 280.7;最少的 187 是天净沙·秋思,最多的 412 是念奴娇·赤壁怀古
动手做(2)为什么平均值一定要包 ROUND先看看不处理会是什么样。
平均值的小数.sqlsql可运行
SELECT AVG(views)                    AS 原始平均值,
       ROUND(AVG(views), 1)          AS 保留一位小数,
       ROUND(AVG(views))             AS 取整,
       ROUND(SUM(likes) / COUNT(*), 1) AS 平均点赞数
FROM article;
查询结果SQL 执行结果
运行结果: 原始平均值 | 保留一位小数 | 取整 | 平均点赞数 280.6667 | 280.7 | 281 | 37.7 -- 共 1 行 -- 整数列做 AVG,MySQL 默认给 4 位小数,可读性差,所以业务上一般都 ROUND 一下 -- 点赞数 42+31+58+47+22+26 = 226,226 ÷ 6 = 37.6666… → 37.7
讲透
  • ROUND(值, 位数):四舍五入。第二位省略就是取整。报表上除了「平均」,「比例、百分比」也都该 ROUND 一下——不然前端会显示出一串 12.804878048780488。
  • AVG = SUM ÷ COUNT(列),注意是除以非 NULL 的个数,不是总行数。所以某列有 NULL 时,AVG 算出来可能比你想的偏大。这个坑第 6 节细讲。
  • MIN / MAX 不只是数字:也能用于字符串和日期。比如 MIN(created_at) 就是「最早注册时间」。字符串按字符顺序比大小,中文这里比的是编码顺序,不是拼音顺序,别指望 MIN(title) 给你「笔画最少」的那篇。
  • 它们都自动跳过 NULL:SUM、AVG、MIN、MAX 遇到 NULL 都当没看见。⚠️ 但有个例外要记住:如果一组里全是 NULL,SUM 返回的是 NULL,不是 0——看板上那些「空白格」就是这么来的。
  • 落到项目里:总浏览量、平均点赞数、最热作品这些数字卡片,一行 SQL 就能出来,不需要在 Python 里循环累加。
避坑别把聚合函数和不聚合的列混在一条查询里,比如 SELECT title, COUNT(*) FROM article——MySQL 在默认的 ONLY_FULL_GROUP_BY 模式下会直接报错 In aggregated query without GROUP BY, expression #1 of SELECT list contains nonaggregated column。原因不难理解:COUNT 只有一个数,而 title 有 6 个,数据库不知道该把哪个 title 配给这个数。想「既看明细又看汇总」,就得用下一节的 GROUP BY,或者拆成两条查询。

GROUP BY:分组统计

按朝代分别算
聚合:从「给我明细」到「给我答案」——GROUP BY 先分组、再每桶算一个数明细行(12 行)article 原始数据唐 · 4 行COUNT 4 · AVG 210宋 · 5 行COUNT 5 · AVG 156魏晋 · 3 行COUNT 3 · AVG 88结果:每组一行dynasty | cnt | avg_views唐 | 4 | 210SELECT dynasty, COUNT(*), AVG(views) FROM article GROUP BY dynasty;规则:SELECT 里的列,要么出现在 GROUP BY 里,要么包进聚合函数——否则数据库不知道归哪组
先讲为什么「全站平均浏览量 280.7」这个数字,对运营来说其实没什么用——他想知道的是「唐诗、宋词、元曲,哪一类更受欢迎」。
这就不是「把 6 行压成 1 个数」了,而是「先按朝代把人分成三堆,每堆各算一个数」。这个「先分组、再各组聚合」的动作,就是 GROUP BY。生活里的例子:期末考完,老师不会只算全班平均分,还要算「每个班的平均分」——先按班级分组。
动手做把 dynasty 放进 GROUP BY,结果就从「一行」变成了「每个朝代一行」。
按朝代分组.sqlsql可运行
SELECT dynasty                     AS 朝代,
       COUNT(*)                    AS 篇数,
       ROUND(AVG(views), 1)        AS 平均浏览量,
       SUM(views)                  AS 总浏览量
FROM article
GROUP BY dynasty
ORDER BY 篇数 DESC;
查询结果SQL 执行结果
运行结果: 朝代 | 篇数 | 平均浏览量 | 总浏览量 唐 | 3 | 262.3 | 787 宋 | 2 | 355.0 | 710 元 | 1 | 187.0 | 187 -- 共 3 行 -- 唐:(328 + 256 + 203) ÷ 3 = 262.3333… → 262.3,合计 787 -- 宋:(412 + 298) ÷ 2 = 355.0,合计 710;元:只有 187,平均就是 187.0 -- 787 + 710 + 187 = 1684,和上一节算的总浏览量对得上
讲透
  • GROUP BY 后面的列叫「分组列」,它决定结果有几行:库里 dynasty 有几种取值,结果就是几行(这里是唐、宋、元三种)。
  • SELECT 里能出现的只有两类东西:① 出现在 GROUP BY 里的分组列;② 包在聚合函数里的表达式。别的列不能出现——因为「唐」这一组里有 3 篇作品,你让它显示哪个 title 呢?
  • 可以按多列分组:GROUP BY dynasty, category_id 就是「同一朝代里再按分类细分」,行数等于「朝代 + 分类」的不同组合数。
  • 执行顺序:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT。看清了:先筛行(WHERE),再分组(GROUP BY),再算聚合(SELECT)。所以聚合函数的结果不可能被 WHERE 用到——这正是下一节 HAVING 存在的原因。
  • 落到项目里:运营后台的「各分类作品量」「各朝代热门度」「每日新增用户趋势图」,全都是 GROUP BY 出来的。趋势图那个更直白:GROUP BY DATE(created_at)。
动手做(2)分组之前先筛行只看「浏览量大于 200」的作品,再按朝代分组。注意会发生什么。
先筛后分组.sqlsql可运行
SELECT dynasty              AS 朝代,
       COUNT(*)             AS 篇数,
       ROUND(AVG(views), 1) AS 平均浏览量
FROM article
WHERE views > 200
GROUP BY dynasty
ORDER BY 篇数 DESC;
查询结果SQL 执行结果
运行结果: 朝代 | 篇数 | 平均浏览量 唐 | 3 | 262.3 宋 | 2 | 355.0 -- 共 2 行 -- 天净沙·秋思(元,187)在 WHERE 就被筛掉了,所以「元」这一组直接消失了 -- WHERE 是「分组之前」的动作:行没了,组自然就没了
避坑分组查询里塞一个「既没分组、也没聚合」的列,是新手最常收到的报错:Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'hiwen.article.title' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by(错误号 1055)。不要靠改 SQL 模式去关掉它,那只是把错误藏起来,出来的 title 到底是哪一篇纯属随机。正确做法是想清楚:这一列该放进 GROUP BY,还是该包进 COUNT/AVG。

HAVING 与 WHERE 的区别

一个筛行,一个筛组
WHERE 筛行,HAVING 筛组:一站之差,作用对象完全不同FROM取全表WHERE筛行:去掉不满足的行GROUP BY把行装进组HAVING筛组:去掉不满足的组SELECT输出结果同一个需求,两种写法:统计平均浏览量超过 100 的朝代❌ WHERE AVG(views) > 100 —— 报错:WHERE 时还没分组,AVG 无从算起✅ GROUP BY dynasty HAVING AVG(views) > 100 —— 先分组,再筛组口诀:行级条件放 WHERE,组级条件放 HAVING
先讲为什么分组之后,运营又说:「篇数少于 2 篇的朝代就别展示了,样本太少没意义」。这个条件是加在「组」上的,不是加在「行」上的。
上一节已经看到:WHERE 在分组之前执行,那时候还没有「篇数」这个概念,所以 WHERE COUNT(*) >= 2 是写不出来的。数据库的解决办法很直接:再加一个专门筛「组」的关键字,叫 HAVING。
动手做(1)用 HAVING 筛掉小样本的组顺便注意:HAVING 里可以直接用 SELECT 里起的别名 篇数,因为 HAVING 在 SELECT 之后执行。
HAVING.sqlsql可运行
SELECT dynasty       AS 朝代,
       COUNT(*)      AS 篇数
FROM article
GROUP BY dynasty
HAVING 篇数 >= 2;
查询结果SQL 执行结果
运行结果: 朝代 | 篇数 唐 | 3 宋 | 2 -- 共 2 行 -- 「元」只有 1 篇,被 HAVING 筛掉了
动手做(2)两个一起用,看清各自的职责「只看已发布的作品(WHERE 管行),并且平均浏览量高于 300 的朝代(HAVING 管组)」。
WHERE-配-HAVING.sqlsql可运行
SELECT dynasty              AS 朝代,
       COUNT(*)             AS 篇数,
       ROUND(AVG(views), 1) AS 平均浏览量
FROM article
WHERE status = 1 -- ① 先筛行:只看已发布的
GROUP BY dynasty -- ② 再分组
HAVING ROUND(AVG(views), 1) > 300; -- ③ 最后筛组:平均浏览量要够高
查询结果SQL 执行结果
运行结果: 朝代 | 篇数 | 平均浏览量 宋 | 2 | 355.0 -- 共 1 行 -- 唐是 262.3,没到 300;元是 187.0,也没到。只有宋以 355.0 过关 -- 本库 6 篇作品的 status 都是 1,所以这里的 WHERE 没有筛掉任何行; -- 但真实站点里草稿(status = 0)很常见,那句 WHERE 就是用来排除它们的
讲透:一张表记住两者的区别
  • WHERE 过滤「行」,HAVING 过滤「组」。WHERE 在分组之前执行,所以能用的只有表里的原始列;HAVING 在分组之后执行,所以可以用聚合函数(COUNT、AVG…)和 SELECT 里的别名。
  • 执行顺序(背下来):FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT。左边先跑、右边后跑,谁先谁后决定了「此刻哪些名字存在」。
  • 能用 WHERE 就优先用 WHERE:它先执行,能让参与分组的数据行数更少,速度快得多。只有当条件里真的带聚合函数(比如「平均浏览量大于 300」「篇数至少 2 篇」)时,才不得不交给 HAVING。
  • 别名的小便利:MySQL 允许 HAVING 和 ORDER BY 直接用 SELECT 里的别名,但 WHERE 和 GROUP BY 不允许(第 3 章已经踩过这个坑)。
避坑聚合函数不能写在 WHERE 里。写 WHERE COUNT(*) >= 2 会报 ERROR 1111 (HY000): Invalid use of group function——不是语法拼错,而是执行顺序上「此刻还不存在 COUNT 的结果」。看到这个错误,立刻把条件从 WHERE 挪到 HAVING。反过来,把普通列条件写进 HAVING 不会报错,但会让数据库白白多处理一批数据,属于「能跑但慢」的写法。

NULL 值处理:IFNULL / COALESCE

别让空值拖低统计结果
先讲为什么上一章你已经知道 NULL 是「没有值」。但在统计场景里,NULL 的麻烦更大:它会被聚合函数跳过,让「平均」悄悄变了味;它还会让 SUM 返回一个 NULL 而不是 0,前端拿到的就是一片空白。
解决办法是把 NULL「换成一个合理值」再做计算,这就要用到 IFNULL 和 COALESCE。
动手做(1)亲眼看看「聚合遇到 NULL」是什么行为书评表的 parent_id 全是 NULL,正好拿来演示。
聚合与NULL.sqlsql可运行
SELECT COUNT(*)                     AS 总行数,
       COUNT(parent_id)             AS COUNT列会跳过NULL,
       SUM(parent_id)               AS SUM全是NULL时返回NULL,
       IFNULL(SUM(parent_id), 0)    AS 用IFNULL兜底成0
FROM comment;
查询结果SQL 执行结果
运行结果: 总行数 | COUNT列会跳过NULL | SUM全是NULL时返回NULL | 用IFNULL兜底成0 3 | 0 | NULL | 0 -- 共 1 行 -- COUNT(parent_id) 数的是非 NULL 值 → 0 -- SUM(parent_id) 因为 3 个值都是 NULL,返回 NULL(不是 0!) -- 套一层 IFNULL(..., 0) 之后,才得到一个前端能直接用的 0
动手做(2)把 NULL 换成给人看的文字「父评论 ID 为 0」不好懂,「顶层评论」才看得明白。
IFNULL-COALESCE.sqlsql可运行
SELECT id,
       IFNULL(parent_id, 0)            AS 父评论ID_IFNULL,
       COALESCE(parent_id, '顶层评论')  AS 评论层级,
       COALESCE(parent_id, 0)          AS 父评论ID_COALESCE
FROM comment;
查询结果SQL 执行结果
运行结果: id | 父评论ID_IFNULL | 评论层级 | 父评论ID_COALESCE 1 | 0 | 顶层评论 | 0 2 | 0 | 顶层评论 | 0 3 | 0 | 顶层评论 | 0 -- 共 3 行 -- 两个函数的作用一样:NULL 就换成后面给的值
讲透
  • IFNULL(值, 兜底值):只判断「是不是 NULL」,是就换成兜底值,不是就原样返回。只有两个参数,是 MySQL 特有的写法。
  • COALESCE(值1, 值2, 值3…):按顺序找第一个不是 NULL 的值返回;括号里可以给任意多个。它是 SQL 标准函数,换成别的数据库也能用,所以更推荐。
  • 聚合函数遇到 NULL 的三条规则:① COUNT(列) 跳过 NULL;② SUM / AVG / MIN / MAX 也跳过 NULL,全为 NULL 时返回 NULL;③ COUNT(*) 不跳,因为它数的是行。
  • 「跳过 NULL」对平均值的实际影响:如果 6 篇作品里有 2 篇的 views 是 NULL,那么 AVG(views) 是「÷ 4」而不是「÷ 6」,算出来会偏高。想让没数据的按 0 算,就得写 AVG(IFNULL(views, 0))。「平均」到底该怎么算,是要先跟业务确认的,不是随手一写的。
  • 落到项目里:用户中心显示「简介」时用 COALESCE(intro, '这个人很懒,什么都没写'),前后端都不用再判断空值了。
避坑别把 NULL 当成 0 直接参与比较。WHERE views + 1 > 300 这样的写法,一旦 views 是 NULL,整个表达式就是 NULL,条件不成立,那一行会被静默丢掉——你不看数据根本发现不了。凡是拿可能为空的列做运算,先包一层 IFNULL(列, 0)。另外注意 IFNULL(views, 0) = 0 会把「浏览量真的是 0」和「还没统计」混成一致,如果业务上要区分这两者,就只能用 IS NULL 单独判断。

综合:每个分类有几篇作品

把聚合、分组、连接串起来
先讲为什么首页左侧的分类导航,每个分类后面都跟着一个数字(唐诗 2、宋词 2……)。这个数字就是分组统计。
但有个细节:我们的库里一共 5 个分类,其中「小说」一篇作品都没有。按 category_id 分组的话,「小说」这一行根本不会出现——可前端明明要显示 5 个分类,只是小说后面跟着 0。该显示 0 的组消失了,这是分组统计里最容易被忽略的一类 bug。这一节就把这个问题彻底解决。
动手做(1)最直接的写法:按 category_id 分组先看看问题长什么样。
按分类ID分组.sqlsql可运行
SELECT category_id AS 分类ID,
       COUNT(*)    AS 篇数
FROM article
GROUP BY category_id
ORDER BY category_id;
查询结果SQL 执行结果
运行结果: 分类ID | 篇数 1 | 2 2 | 2 3 | 1 4 | 1 -- 共 4 行 -- 只有 4 行!因为 article 表里没有任何一条记录的 category_id 是 5 -- 「小说」这个分类在本次统计里被彻底漏掉了,而它其实应该是 0 篇
动手做(2)以 category 为主表左连接,把 5 个分类一个不少地列出来这里提前用到了第 5 章的 LEFT JOIN:左表(category)每一行都保留,右表(article)没有匹配上的地方填 NULL。
每个分类篇数.sqlsql可运行
SELECT c.id        AS 分类ID,
       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;
查询结果SQL 执行结果
运行结果: 分类ID | 分类名 | 篇数 1 | 唐诗 | 2 2 | 宋词 | 2 3 | 元曲 | 1 4 | 散文 | 1 5 | 小说 | 0 -- 共 5 行 -- 「小说」一行出现了,篇数是 0 —— 这就是页面上要的效果 -- 关键是 COUNT(a.id):小说那一行 a.id 全是 NULL,而 COUNT(列) 会跳过 NULL,所以得 0
讲透
  • 为什么必须 COUNT(a.id) 而不是 COUNT(*):LEFT JOIN 之后,「小说」那一行其实也有一行记录(只是 a 的字段全是 NULL)。COUNT(*) 数行 → 得 1,结果就错了;COUNT(a.id) 数「有值的 a.id」→ NULL 跳过 → 得 0,这才是我们要的。
  • 这就是第 2 节那条规则的实战用法:「数总行数用 COUNT(*),数某一列有值的个数用 COUNT(列)」。在 LEFT JOIN 场景下,它们数出来的结果注定不一样。
  • GROUP BY 里为什么要带上 c.name:因为它出现在 SELECT 里,属于「既没聚合也没分组的列」,会被 ONLY_FULL_GROUP_BY 拦下来。带上它,语义上也更清楚。
  • 落到项目里:这段查询的结果,就是前端分类导航里「唐诗(2) 宋词(2) 元曲(1) 散文(1) 小说(0)」那排数字的来源。换到「红影赏析」,把 category 换成电影类型表、article 换成 movie,逻辑一个字都不用改。
避坑只要一句查询里同时出现 LEFT JOIN 和 COUNT,就一定要问自己一句:「我数的到底是有记录的行,还是右边表真的有数据?」把 COUNT(a.id) 写成 COUNT(*),会让所有「空分类」都显示成 1 篇,而且不报错、不提醒,只给你一个错数字。同类的还有 SUM:SUM(a.views) 在空分类上是 NULL 而不是 0,页面上会出现空白,记得套 IFNULL(SUM(a.views), 0)。
动手练习
1用一条 SQL 统计全站作品的总篇数、总浏览量、平均浏览量(平均保留 1 位小数),并用其余两个数的关系把平均值的算法解释一遍。
2按「是否首页推荐」(is_recommend)分组,统计推荐作品与普通作品各有几篇、平均浏览量多少(平均保留 1 位小数)。
3迁移练习:「红影赏析」的 movie 表字段是 id、title、genre(类型)、rating(评分)。请统计每个类型有几部电影、平均评分多少(平均保留 1 位小数),并且只保留电影数不少于 2 部的类型。写完后想一想:这个「不少于 2 部」的条件为什么必须写 HAVING 而不是 WHERE?
参考答案
练习答案.sqlsql可运行
-- 第 1 题
SELECT COUNT(*)           AS 总篇数,
       SUM(views)         AS 总浏览量,
       ROUND(AVG(views), 1) AS 平均浏览量
FROM article;

-- 第 2 题
SELECT is_recommend              AS 是否推荐,
       COUNT(*)                  AS 篇数,
       ROUND(AVG(views), 1)      AS 平均浏览量
FROM article
GROUP BY is_recommend
ORDER BY is_recommend DESC;

-- 第 3 题(红影赏析的库,表名换成 movie)
SELECT genre                     AS 类型,
       COUNT(*)                  AS 电影数,
       ROUND(AVG(rating), 1)     AS 平均评分
FROM movie
GROUP BY genre
HAVING COUNT(*) >= 2
ORDER BY 电影数 DESC;
查询结果SQL 执行结果
运行结果: -- 第 1 题 总篇数 | 总浏览量 | 平均浏览量 6 | 1684 | 280.7 -- 共 1 行 -- 1684 ÷ 6 = 280.6666…,所以平均是 280.7 -- 第 2 题 是否推荐 | 篇数 | 平均浏览量 1 | 3 | 332.0 0 | 3 | 229.3 -- 共 2 行 -- 推荐的 3 篇:(328 + 256 + 412) ÷ 3 = 332.0 -- 普通的 3 篇:(298 + 187 + 203) ÷ 3 = 229.3333… → 229.3 -- 第 3 题(在红影的库里执行) 类型 | 电影数 | 平均评分 科幻 | 3 | 8.6 -- 共 1 行 -- 具体数字以红影库里的数据为准

第 3 题的关键:「电影数不少于 2 部」这个条件是对「组」提的,而分组发生在 WHERE 之后,所以 WHERE 里根本没有 COUNT(*) 可用(写了会报 Invalid use of group function)。判断口诀:条件里带不带聚合函数——不带就放 WHERE,带了就放 HAVING。

本章小结