SQL 聚合
SQL 聚合将输入行计算为计数、总额或平均值。本文负责 MySQL 8.0 的分组、过滤、空值和统计粒度。
1. 面试核心
先记住这几条,再读正文- 行数与非空值计数。
COUNT(*)统计行数,COUNT(column)统计该列非空的行数。卡 · 面试核心 - 过滤阶段。 WHERE 过滤输入行,HAVING 过滤聚合后的组。卡 · 分组前后过滤
- 空输入。 无 GROUP BY 的空输入聚合返回一行,COUNT 为 0,SUM 为 NULL。卡 · 分组前后过滤
- 去重计数。
COUNT(DISTINCT column)统计非空值的去重数量。卡 · 条件聚合与去重计数 - 分组取列。
ONLY_FULL_GROUP_BY要求非聚合列有分组或函数依赖等依据。卡 · 分组列与函数依赖
2. 分组与 NULL
GROUP BY 指定每个结果组的粒度。
CREATE TABLE sales (
id INTEGER PRIMARY KEY, team VARCHAR(20), amount INTEGER
);
INSERT INTO sales VALUES
(1, 'A', 30), (2, 'A', NULL), (3, 'A', 70), (4, 'B', 20);
SELECT team, COUNT(*) AS rows_count,
COUNT(amount) AS known_count, SUM(amount) AS total
FROM sales GROUP BY team ORDER BY team;
A 组得到 (A,3,2,100),B 组得到 (B,1,1,20)。SUM 和 COUNT(amount) 忽略空金额。COUNT(*) 仍计入该行。A 组的 AVG(amount) 是 50,分母为两个非空值。
多个 NULL 分组键归入同一组。结果组的展示顺序由 ORDER BY 明确指定。
3. 分组列与函数依赖
分组后的每个输出值都需有明确来源。聚合函数从组内多行计算结果。非聚合列则需由分组键唯一确定,或满足单值过滤条件。
SELECT id, team, SUM(amount) FROM sales GROUP BY id;
-- id 是主键,每组只有一行,因此 team 可由 id 确定。
-- 执行错误:team 无法唯一确定 id。
-- SELECT team, id, SUM(amount) FROM sales GROUP BY team;
MySQL 8.0 默认 ONLY_FULL_GROUP_BY 会拒绝第二种查询。关闭该模式后,服务器可从组内任选一个 id。结果不再稳定,ORDER BY 也无法指定被选中的组内值。
4. 分组前后过滤
WHERE 决定哪些输入行参加统计。HAVING 再决定哪些组出现在结果中。
SELECT team, SUM(amount) AS total FROM sales
WHERE amount >= 30
GROUP BY team
HAVING SUM(amount) >= 100
ORDER BY team;
WHERE 留下 A 的 30 和 70。聚合后总额为 100,满足 HAVING。最终结果只有 (A,100)。将行条件改为组条件,会改变参与统计的数据。
5. 空输入与空值
无 GROUP BY 时,整个输入视为一组。即使输入为空,也返回一行聚合结果。
SELECT COUNT(*) AS n, SUM(amount) AS total
FROM sales WHERE id < 0;
SELECT team, COUNT(*) FROM sales WHERE id < 0 GROUP BY team;
第一条返回 (0,NULL),第二条返回空结果集。第二条没有输入行,因此也没有分组键。
需要将“没有金额”记为 0 时,可用 COALESCE(SUM(amount),0)。未知金额是否也应变成 0,要由业务定义。
6. 条件聚合与去重计数
CASE 可以在同一组内计算不同条件的统计值。
SELECT team,
SUM(CASE WHEN amount >= 50 THEN 1 ELSE 0 END) AS high_count,
COUNT(DISTINCT amount) AS distinct_amounts
FROM sales GROUP BY team ORDER BY team;
结果为 (A,1,2)、(B,0,1)。CASE 将符合条件的行记为 1,其余记为 0。COUNT(DISTINCT amount) 对非空金额去重。
COUNT(CASE WHEN amount>=50 THEN 1 ELSE 0 END) 会统计所有行。原因是 0 也属于非空值。使用 COUNT 做条件计数时,应让未匹配分支返回 NULL。
7. 使用边界
非聚合输出列需要明确的取值依据。具体检查规则见分组列与函数依赖。
LEFT JOIN 后,使用 COUNT(右表非空主键) 统计匹配数。连接补空与明细放大的机制见 SQL 连接。
8 · 面试题卡片
本页题目与参考回答
8.1 COUNT(*) 和 COUNT(column) 有什么区别,LEFT JOIN 后怎样统计右表记录数?
考察重点:输入行粒度、NULL 处理与外连接补空行的计数。
参考回答
COUNT(*) 计入所有结果行,COUNT(column) 只计非空值。
LEFT JOIN 的补空行也占一行。因此,应统计右表的非空主键。没有匹配记录的左行便得到 0。多表连接产生重复明细时,还需先修正统计粒度。
8.2 WHERE 与 HAVING 如何影响聚合,空输入会返回什么?
考察重点:行过滤、组过滤与空结果。
参考回答
WHERE 在分组前筛行,HAVING 在聚合后筛组。
无 GROUP BY 的空输入聚合仍返回一行。COUNT 是 0,SUM 是 NULL。带 GROUP BY 时,空输入没有任何组,返回空结果集。
8.3 怎样统计每组满足条件的行数,COUNT 的 ELSE 0 为什么有问题?
考察重点:CASE、非空计数和 DISTINCT。
相关内容:条件聚合与去重计数。
参考回答
可用 SUM(CASE WHEN 条件 THEN 1 ELSE 0 END)。
它将命中行累加为数量。COUNT 会计入 0,因此其未命中分支应返回 NULL。COUNT(DISTINCT 列) 则统计去重后的非空值数量。
8.4 GROUP BY 后为什么不能随意选择其他列,按主键分组有何不同?
考察重点:结果确定性、函数依赖和 SQL 模式。
相关内容:分组列与函数依赖。
参考回答
同一组内的其他列可有多个值,需要明确选择依据。
主键唯一确定一行,因此其余列依赖主键。MySQL 8.0 允许这种函数依赖。仅按 team 分组时,多行的 id 不唯一,默认模式会拒绝查询。