AVG / SUM / MIN / MAX 在面对 NULL 时的行为?
How do AVG / SUM / MIN / MAX handle NULL values?
考察要点
这道题面试官想看你是否精确理解 SQL 标准中聚合函数处理 NULL 值的核心规则,尤其是在 AVG 函数上的体现。这不仅考察你的基础知识是否扎实,也考察你对数据计算严谨性的关注,因为错误理解该规则会导致严重的统计偏差。
核心概念
SQL 标准规定,除了 COUNT(*) 之外,所有的聚合函数在计算时都会忽略 NULL 值。
我们通过一个具体的例子来理解:
假设有一张 student_scores 表:
| id | name | score |
|---|---|---|
| 1 | Alice | 100 |
| 2 | Bob | 80 |
| 3 | Carol | NULL |
| 4 | Dave | 90 |
| 5 | Eve | NULL |
1. SUM(score)
计算非 NULL 值的总和。
- 计算过程:
100 + 80 + 90 = 270 - 结果:
270
2. MIN(score) / MAX(score)
计算非 NULL 值中的最小值/最大值。
MIN计算过程:MIN(100, 80, 90)->80MAX计算过程:MAX(100, 80, 90)->100
3. AVG(score)
这是最容易出错的地方。AVG(col) 的内部计算公式是 SUM(col) / COUNT(col)。由于 SUM 和 COUNT(col) 都忽略 NULL,所以:
SUM(score):270COUNT(score):3(只计算了 Alice, Bob, Dave 的分数)- 计算过程:
270 / 3 = 90 - 结果:
90
4. COUNT 的变体
COUNT(score): 计算score列中非NULL值的行数。结果为3。COUNT(*): 计算表中的总行数,无论任何列的值是否为NULL。结果为5。COUNT(1): 效果与COUNT(*)相同,在大多数现代数据库(如 MySQLInnoDB)中,性能也几乎没有差异。它计算的是值为1这个常量表达式的行数,也就是所有行。
边界条件与差异:
-
空表或全为 NULL:
- 如果聚合的集合为空(例如,一个空表,或者
WHERE条件过滤掉了所有行),SUM,AVG,MIN,MAX均返回NULL,而COUNT返回0。 - 如果聚合的列值全部为
NULL(如上例中的Carol和Eve组成一个分组),SUM,AVG,MIN,MAX同样返回NULL,COUNT(col)返回0。
- 如果聚合的集合为空(例如,一个空表,或者
-
数据库兼容性:
- 此行为是 SQL-92 标准的一部分,因此在 MySQL, PostgreSQL, Oracle, SQL Server 等主流关系型数据库中表现一致。
常见低分回答
-
误认为
AVG会将NULL当作0处理。这是一个非常普遍的错误。- 错误计算:
(100 + 80 + 0 + 90 + 0) / 5 = 270 / 5 = 54 - 正确计算:
(100 + 80 + 90) / 3 = 90 - 面试官点评: 这个错误直接反映了候选人对聚合函数核心机制的理解偏差,是严重的扣分项。
- 错误计算:
-
混淆
COUNT(*)和COUNT(column)。- 错误描述: "COUNT 都是用来计行数的,所以
COUNT(*)和COUNT(score)结果应该差不多。" - 正确区分:
COUNT(*)统计物理行数,COUNT(column)统计指定列上非NULL的值的个数。在我们的例子中,前者是5,后者是3。
- 错误描述: "COUNT 都是用来计行数的,所以
-
认为对全为
NULL的列求SUM返回0。- 错误 SQL 及预期:
SELECT SUM(score) FROM student_scores WHERE name IN ('Carol', 'Eve');错误地认为结果是0。 - 正确结果:
NULL。因为SUM忽略了所有NULL值后,发现没有可计算的数值,所以返回NULL。
- 错误 SQL 及预期:
-
在
GROUP BY场景下描述不清。当被问及分组聚合时,无法清晰说明如果一个分组内全是NULL会发生什么。正确的回答是,该分组依然会输出一行,但其聚合结果(SUM/AVG/MIN/MAX)为NULL。
工程实践与优化
-
数据建模阶段: 在设计表结构时,应明确列是否允许为
NULL。如果一个字段(如:score)在业务逻辑上不应该为空,就应该定义为NOT NULL DEFAULT 0。这可以从根本上避免聚合函数对NULL处理的歧义,保证数据一致性。 -
显式处理
NULL: 如果业务需要将NULL视为0来计算平均值,必须显式地使用函数进行转换。sql1-- 将 NULL 替换为 0 后再计算平均值2-- COALESCE 是标准 SQL 函数,推荐使用3SELECT AVG(COALESCE(score, 0)) FROM student_scores;4-- 在 MySQL 中也可以用 IFNULL5SELECT AVG(IFNULL(score, 0)) FROM student_scores;注意: 在
WHERE子句中对列使用函数(如WHERE COALESCE(score, 0) > 60)通常会导致索引失效,但在SELECT部分的聚合计算中,由于通常需要全表或索引扫描,性能影响相对较小。 -
执行计划分析:
- 对于
SELECT AVG(score) FROM student_scores;,EXPLAIN通常会显示type: ALL(全表扫描) 或type: index(全索引扫描)。 - 如果
score列上有索引,EXPLAIN的Extra字段可能会显示Using index,表示数据库只需扫描索引文件,无需回表查询数据行,这是最高效的聚合方式(覆盖索引)。 - 对于
MIN(indexed_col)或MAX(indexed_col),如果该列是索引的最左前缀,优化器可以直接从索引的头部或尾部获取值,无需扫描,复杂度为 。
- 对于
常见追问与延伸题目
-
追问:如果我想把
NULL分数当成0分计入总平均分,SQL 该怎么写?- 回答要点: 需要使用
COALESCE或IFNULL函数。正确的写法是SELECT AVG(COALESCE(score, 0)) FROM student_scores;。这会计算(100 + 80 + 0 + 90 + 0) / 5 = 54。进一步解释,COALESCE(expression, ...)返回参数列表中的第一个非NULL值,是跨数据库的标准用法,而IFNULL(expr1, expr2)是 MySQL 特有的,功能类似但只接受两个参数。
- 回答要点: 需要使用
-
追问:
COUNT(*)、COUNT(1)和COUNT(column)的区别和性能差异?- 回答要点:
COUNT(*): SQL 标准推荐的行数统计方式,统计所有行。COUNT(1): 效果同COUNT(*),因为1是一个非空常量。在 MySQL 的InnoDB引擎中,优化器会将COUNT(1)自动优化为COUNT(*),所以没有性能差异。COUNT(column): 只统计指定column中非NULL的行数。- 性能:
COUNT(*)和COUNT(1)性能基本一致。COUNT(column)如果column是非索引列,性能会低于COUNT(*);如果column是索引,性能可能与COUNT(*)相当或更快(取决于索引类型和存储引擎)。在MyISAM引擎中,COUNT(*)对整表计数非常快,因为它直接读取存储的行数,但带WHERE条件后就没这个优势了。InnoDB则需要扫描。
- 回答要点:
-
追问:如果我用
SUM(score) / COUNT(*)来计算平均分,和AVG(score)有什么区别?- 回答要点: 这是考察你是否真正理解了
AVG的核心。AVG(score)的分母是非NULL分数的数量 (COUNT(score))。SUM(score) / COUNT(*)的分母是总行数。- 在存在
NULL值的情况下,COUNT(score)<COUNT(*),因此AVG(score)的结果会大于SUM(score) / COUNT(*)。这两种计算方式代表了不同的业务口径:前者是“有效分数的平均分”,后者是“将缺考(NULL)视为 0 分后,全体学生的平均分”(假设SUM对NULL的处理符合预期,但实际上SUM忽略NULL,所以这种写法本身就是一种逻辑陷阱,正确的“视为0分”写法是SUM(COALESCE(score,0)) / COUNT(*))。
- 回答要点: 这是考察你是否真正理解了