news 2026/10/13 4:34:32

SQL17 计算男生人数及平均GPA:COUNT、AVG与NULL陷阱详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SQL17 计算男生人数及平均GPA:COUNT、AVG与NULL陷阱详解

我最近在帮几个准备面试的同学复盘SQL刷题记录,几乎每次都会聊到“SQL17 计算男生人数以及平均GPA”这道题。乍一看,题目简单到像是“送分题”,可等我真正去检查他们提交的答案,才发现大部分人都在细节上栽了跟头,比如GPA精度不对、NULL值被算错、字段命名不符合预期。这道题表面只考两件事:统计男生人数、计算男生平均GPA,实际却把COUNT、AVG、WHERE、GROUP BY、ROUND、NULL三值逻辑全部串在了一起。对于刚接触SQL的新手来说,它是理解聚合函数和条件过滤的最佳练习;对于准备面试的求职者来说,它又是一道可以无限扩展的经典母题。这篇文章我就从题目拆解、写法推演、精度与NULL处理、常见错误排查、面试变体这五个维度,把这道题彻底讲透。

1. 题目拆解:这题到底在考什么

1.1 需求场景与常见表结构

先明确业务场景。绝大多数在线练习平台和面试题里,“SQL17 计算男生人数以及平均GPA”都会对应一张学生信息表,通常叫student或t_student,字段大同小异,最常见的是这么几个:

CREATE TABLE student ( id INT PRIMARY KEY, name VARCHAR(50), gender VARCHAR(10), gpa DECIMAL(3, 1) );

这里的gpa是绩点,常见范围是 0 到 4.0 或 0 到 5.0,具体看学校算法,题目的评分一般不会在这个范围上较真,但表结构里通常会强调它是数值类型,而且可能包含 NULL——比如某些学生没有录入绩点。gender字段通常是字符串类型,取值为“男”和“女”,有些平台会用'M'、'F',甚至数字1、0来表示。这一点很重要,因为不同的取值方式会直接影响你的过滤条件怎么写。

我看到不少同学做题时只看题目描述,不看表结构,上来就是WHERE gender = '男',结果在真实面试环境里可能字段是gender但存的是'M',或者字段名直接叫sex,就容易踩坑。所以拆题的第一步永远不是写SQL,而是先弄清楚“表里到底有哪些字段、字段类型是什么、取值有哪些可能、有没有NULL”。

1.2 三个容易被忽略的隐藏考点

这个题目看似只要求算两个值,但隐藏考点至少有三个方面。

第一个隐藏考点是“条件聚合”的理解。你需要在计算AVG(gpa)前先把数据限定在男生这个子集里,这考查的是WHERE与聚合函数的配合顺序。很多人会下意识地认为必须要用GROUP BY gender才能按性别算,实际上题目只要求“男生人数”和“男生平均GPA”,不要求同时输出女生数据,那么直接前置过滤更简洁。够不够用GROUP BY?也可以用,但会导致输出结构变化,这个差异我会在后面的写法章节详细展开。

第二个隐藏考点是“平均值要不要四舍五入”。绝大多数平台的预期结果要求保留一位小数,比如 3.6,或者保留两位,比如 3.55。那你就得用ROUND()函数显式控制精度,否则数据库默认输出的可能是3.6000000000000005这种浮点噪声,判题系统直接判错。我看到过太多人忽略了这一点,输出了长长一串小数。

第三个隐藏考点是“列名到底叫什么”。题目通常会写“结果包含两列:男生人数与平均GPA”,但不同平台对列名要求不一样,有的是male_count,有的是cnt,有的是中文别名。如果你不用别名,数据库自动生成的列名可能是COUNT(*)和AVG(gpa),这在人工阅卷时通常能接受,但在自动判题系统里很容易因为列名对不上而判错。

这三个隐藏考点加起来,难度就从一个“纯写法题”变成了“写对 + 符合输出规范”的细节题。这也是为什么刷题平台上简单题的通过率往往没有想象中那么高的原因。

2. 从零手写:两种核心写法的完整思路

2.1 方案一:WHERE过滤 + 两个聚合函数

先说最推荐、也最直观的写法。

SELECT COUNT(*) AS male_count, ROUND(AVG(gpa), 1) AS avg_gpa FROM student WHERE gender = '男';

这个SQL的逻辑非常清晰:先通过WHERE gender = '男'把全表数据过滤为“仅男生”,再对过滤后的数据集执行COUNT(*)和AVG(gpa)。整个过程相当于把一个相对复杂的任务拆成了两步,你先看到的是“男生子集”,然后所有的聚合都发生在这个子集上,完全不需要考虑女生的干扰。

为什么我推荐这种写法?因为对于大多数刚入门SQL的同学来说,理解“先过滤、再聚合”比理解“条件聚合”要容易得多。它也更贴合实际业务场景:你只需要关注一个群体的指标,不需要输出一个按性别分组的大宽表。这种写法在逻辑上没有任何多余动作,性能上也通常是更优的,因为聚合之前就减少了数据量。

不过要提醒一点,这里的COUNT(*)统计的是行数,不是某个非空字段的个数。只要一行数据存在,无论gpa是否为 NULL,这行都会计入人数。这一点恰恰是本题最容易混淆的地方。比如你想写COUNT(gpa)来数男生人数,那么一旦某个男生的gpa是 NULL,这一行就会被COUNT(gpa)丢掉,结果就错了。所以人数统计一定要用COUNT(*)或COUNT(1),不要用某个可能为 NULL 的字段。

2.2 方案二:CASE WHEN 条件聚合

另一种写法是使用条件聚合,不依赖WHERE,而是把“男生”的判断放到聚合函数内部。

SELECT SUM(CASE WHEN gender = '男' THEN 1 ELSE 0 END) AS male_count, ROUND(AVG(CASE WHEN gender = '男' THEN gpa ELSE NULL END), 1) AS avg_gpa FROM student;

这个写法第一次看到时可能会觉得绕,但它的核心思想很值得学习:对全表数据一次性扫描,在SUM内部通过CASE WHEN将符合条件的行标记为 1,其余标记为 0,然后求和;平均GPA同理,符合条件的保留gpa,不符合的直接置为 NULL,这样AVG就会自动忽略 NULL 值。

为什么这个写法在某些场景下非常有价值?因为当你需要在一行结果里同时输出多个分组的指标时,比如“男生人数、女生人数、总人数、男生平均GPA、女生平均GPA”,WHERE写法就得写好几遍子查询或者UNION,而SUM(CASE WHEN)可以在一次扫描里完成所有统计。这是数据仓库领域非常常见的“行转列”技巧。

但我也要坦白说,对于“SQL17”这个原题本身,条件聚合属于“可以但没必要”的写法。它至少能训练一种思维:聚合函数不一定要搭配GROUP BY,也不一定要先过滤,它可以对全表做条件式统计。如果你希望自己对SQL的理解更进一步,建议两种写法都在本地环境跑一下,观察输出结果是否一致。

2.3 SQL执行顺序:先过滤还是先聚合

我见过很多人写SQL时不关心执行顺序,觉得只要结果对就行。但“SQL17”这道题恰恰能帮助你建立执行顺序的直觉。

SQL的书写顺序和实际执行顺序是不同的,这是新手最容易产生困惑的地方。一个典型的查询语句,用简化视角看,执行顺序大概是:先FROM确定表,再WHERE过滤行,然后GROUP BY分组,接着执行聚合函数,最后SELECT投影输出列,最后ORDER BY排序。注意,SELECT里写的别名,在WHERE阶段还不存在,所以你不能在WHERE里引用SELECT中定义的别名。

把这条规则套到方案一里:

SELECT COUNT(*) FROM student WHERE gender = '男';

这里的执行顺序是先扫描student,按gender = '男'过滤出数据子集,再对这个子集执行COUNT(*)。所以你可以理解为“人数是过滤之后才数的”。如果先执行COUNT(*)再过滤,那得到的就是全表人数,逻辑就彻底反了。

方案二则不同,它没有WHERE过滤,所以聚合的输入是全表数据。但由于CASE WHEN的存在,每个聚合函数内部都自带了一个过滤逻辑,把非目标行变成了 0 或 NULL。这种方式相当于“把过滤下沉到聚合内部”,结果是相同的,执行策略却完全不同。理解这一点之后,你在面对“能不能不用WHERE直接聚合”这类问题时,就不会再一脸茫然。

3. GPA精度与NULL值:最容易翻车的地方

3.1 AVG与NULL的三值逻辑

先丢一个看似简单却暗藏杀机的问题:如果男生的gpa有一行是 NULL,AVG(gpa)到底把 NULL 当 0 还是直接忽略?

答案是忽略。SQL 里 NULL 既不是 0,也不是空字符串,它是“未知”的三种取值之一。AVG(gpa)在计算平均值时,会老老实实跳过 NULL 行,只有非 NULL 的数值参与求和与计数。举个例子,假设有三个男生,GPA 分别是 3.2、3.8 和 NULL,那么AVG(gpa)的结果是(3.2 + 3.8) / 2 = 3.5,而不是(3.2 + 3.8 + 0) / 3 = 2.33。

这一点和COUNT的语义要放在一起理解。COUNT(*)是数行数,NULL 也无所谓,它是行级统计;COUNT(gpa)是数非 NULL 的字段个数,它会自动忽略 NULL;AVG(gpa)本质上是SUM(gpa) / COUNT(gpa),分母只统计非 NULL 的数量。所以同样是面对一行 NULL 数据,COUNT(*)可能输出 3,而COUNT(gpa)可能输出 2,结果完全不同。

在“SQL17”这个题目中,如果平台没有明确说GPA无NULL,那么最稳妥的做法是:人数用COUNT(*),平均GPA用AVG(gpa),两者各自遵循NULL规则,在绝大多数情况下都能得到符合预期且准确无误的结果。千万不要自作主张去写AVG(IFNULL(gpa, 0)),因为那会把 NULL 当成 0 参与平均,在业务语义上通常是不合理的。

3.2 ROUND保留位数与整数除法陷阱

平均GPA算出来之后,最容易被判题系统挑刺的就是精度。AVG(gpa)在不做任何处理时,很多数据库会返回非常长的浮点数,比如3.5666666666666664。题目的预期输出可能是3.6或者3.57,所以我几乎总是建议在计算平均值时顺手加一层ROUND。

-- 保留两位小数 ROUND(AVG(gpa), 2) AS avg_gpa -- 保留一位小数 ROUND(AVG(gpa), 1) AS avg_gpa

ROUND的第二个参数就是保留位数,这是最直观的精度控制。但如果你的数据库里gpa字段本身就是整数类型,那就要小心一点:AVG(整数列)在不同数据库里的行为不一样。比如在 MySQL 里,AVG(3)和AVG(4)得到的是3.5,它不会发生整数除法;但如果你自己写SUM(gpa) / COUNT(*),在 SQLite 里如果两个操作数都是整数,结果是整数除法,7 / 2会得到3而不是3.5,这就是著名的“整数除法陷阱”。

所以,如果题目允许,优先直接使用AVG(gpa),而不是自己拆成SUM / COUNT。如果必须手动计算均值,就确保至少有一个操作数是数值常量或做了类型转换,比如SUM(gpa) * 1.0 / COUNT(*),这样除法才会按浮点数执行。这个坑在刷题时不太容易遇到,但在实际业务里,尤其是报表开发中特别常见。

3.3 输出格式与字段命名规范

很多刷题平台对输出列名有严格要求。我见过一个判题规则:结果集列名必须完全匹配预期格式,否则即使数值正确也会显示“答案错误”。所以写SQL的时候,不要偷懒,尽量把别名显式写好。

对于“SQL17”,平台常见的预期输出是这样的:

male_countavg_gpa
43.6

那我就会这样写:

SELECT COUNT(*) AS male_count, ROUND(AVG(gpa), 1) AS avg_gpa FROM student WHERE gender = '男';

有些平台习惯用中文列名,比如“男生人数”和“平均GPA”,那你把AS后面的别名改成对应中文即可。这个看似格式层面的东西,恰恰是自动判题系统的硬性门槛,做题前最好先看一眼题目附带的输出格式说明,而不是充满自信地提交一个“数值正确但列名随意”的答案。

4. 常见错误与排查技巧实录

4.1 五类经典错误对照表

我把过去半年里帮别人排查这道题时见到的典型错误整理成了速查表,基本覆盖了90%的翻车场景。

错误类型错误写法示例问题所在正确写法
忘记过滤条件SELECT COUNT(*), AVG(gpa) FROM student;统计的是全部学生,不是男生加WHERE gender = '男'
用COUNT(gpa)数人数SELECT COUNT(gpa) FROM student WHERE gender = '男';gpa为NULL的行会被忽略,人数偏少改用COUNT(*)或COUNT(1)
直接求和除以计数SELECT SUM(gpa) / COUNT(*) FROM student WHERE gender = '男';整数除法可能得到截断结果先ROUND(AVG(gpa), 1)或*1.0
该用WHERE却用HAVINGSELECT COUNT(*) FROM student HAVING gender = '男';HAVING无法直接引用非分组列,逻辑混乱使用WHERE过滤
忘了ROUNDSELECT AVG(gpa) ...输出过长浮点数,不符合预期格式ROUND(AVG(gpa), 1)

这张表里每个错误都不是“看不懂SQL”造成的,而是“对语义和函数行为理解不到位”造成的。尤其是第二种错误,用COUNT(gpa)来数人数,这种写法的误导性很强,因为题目问的是“人数”,很多人会理所当然地觉得“数一下GPA非空的人不就是人数了吗”,完全忽略了 GPA 为 NULL 的场景。

4.2 一个排查思路实例

有一次,某个同学交上来的答案是:

SELECT COUNT(gender) AS male_count, AVG(gpa) AS avg_gpa FROM student WHERE gender = '男';

他觉得很奇怪,明明逻辑看起来没什么问题,为什么判题系统显示“平均GPA保留位数不符”。我们一步步排查才发现,平台预期保留两位小数,而他没有加ROUND。很多数据库输出AVG(gpa)时会带出一大串浮点尾巴,比如某条数据算出3.7000000000000002,跟预期的3.70自然对不上。加上ROUND(AVG(gpa), 2)之后立刻通过。

另一个案例是列名问题。他把别名写成了maleNum,而平台预期的是male_count,数值正确也被判错了。这种问题最冤枉,因为它跟SQL技术本身无关,纯粹是读题不仔细。从此之后我养成一个习惯:凡是刷题,先看输出描述,再看表结构,最后才动手写SQL。这一点对于“SQL17”这种入门题来说尤其适用,因为它的坑从来都不在“会不会写聚合函数”上,而在那些容易被忽略的输出规范上。

5. 面试实战:从这一题延伸到一类题

5.1 变体一:按性别分组统计

如果面试官把题目稍微升级一下,改为“分别统计男生和女生的人数以及平均GPA”,那就要引入GROUP BY了。

SELECT gender, COUNT(*) AS student_count, ROUND(AVG(gpa), 1) AS avg_gpa FROM student GROUP BY gender;

这个查询会把学生按性别分成两组,然后对每组分别计数、求平均,输出会是两行:

genderstudent_countavg_gpa
男43.6
女33.9

从“SQL17”到这一变体,核心区别在于你是否需要“一次性看到所有分组的统计结果”。如果只需要看一个分组,WHERE更直接;如果需要看全量分组,GROUP BY是必然选择。这里还有一个小坑:GROUP BY gender之后,SELECT里只能出现被分组的字段和聚合函数,其他字段(比如name)不能随便出现,否则在不同数据库里要么报错,要么返回语义不确定的结果。

5.2 变体二:条件计数与HAVING组合

另一个高频变体是“统计平均GPA大于3.5的性别分组”。这时候WHERE已经不能用了,因为AVG(gpa) > 3.5是针对分组后的聚合结果做过滤,必须用HAVING。

SELECT gender, COUNT(*) AS student_count, ROUND(AVG(gpa), 1) AS avg_gpa FROM student GROUP BY gender HAVING AVG(gpa) > 3.5;

回答这类问题时,最好能说清楚WHERE和HAVING的本质区别:WHERE在分组之前过滤原始行,HAVING在分组之后过滤聚合结果。很多人会背“WHERE过滤行,HAVING过滤组”,但只有真正把“SQL17”这类题目倒推一遍,理解了执行顺序,才能把这句话变成自己的知识,而不是死记硬背。

5.3 变体三:多表JOIN场景

再往上走一步,面试官可能会把学生表和学生成绩表拆开,比如:

  • student(id, name, gender)
  • score(student_id, gpa)

那“计算男生人数以及平均GPA”就变成了一道多表关联题:

SELECT COUNT(DISTINCT s.id) AS male_count, ROUND(AVG(sc.gpa), 1) AS avg_gpa FROM student s LEFT JOIN score sc ON s.id = sc.student_id WHERE s.gender = '男';

这里多了两个新考点:一是JOIN之后行数可能因一对多关系而膨胀,人数统计必须用COUNT(DISTINCT s.id),否则一个男生有3条成绩记录就会被数成3个人;二是如果某些男生没有成绩记录,LEFT JOIN之后sc.gpa是 NULL,AVG(sc.gpa)会自动忽略,但如果用COUNT(sc.gpa)数人数就会漏人。

从“SQL17”到多表JOIN,是一个很好的能力递进路径:先掌握单表条件聚合,再掌握分组统计,然后掌握JOIN对聚合的影响。这一个过程走完,基本就把聚合查询的核心骨架搭完整了。

作为过来人,我的体会是:把简单题吃透比盲目刷十道新题更有价值。每当我带新人,我都会建议他们不要急着追求刷题数量,而是拿出一道像“SQL17”这样的小题,把每种写法跑一遍,把其中的NULL陷阱、ROUND精度、执行顺序、HAVING与WHERE的区别全部捋清楚。这个过程看起来很慢,但它构建的是SQL思维的地基。之后你再去面对窗口函数、CTE、复杂JOIN聚合,都会因为基础够扎实而轻松很多。最后再分享一个小技巧:刷题时如果判题系统不通过,先别急着改SQL,把自己写的查询结果和预期输出逐列对比,数值不对优先检查过滤条件,小数不对优先检查ROUND,列名不对优先检查别名,按这个顺序排查,绝大多数问题都能在几分钟内定到位。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 4:33:02

MySQL锁机制详解:从行锁到死锁排查的实战指南

1. 从一次线上锁等待排查说起:MySQL锁到底锁住了什么先说一个我真实处理过的线上问题,它让我彻底把MySQL锁机制重新认真过了一遍。某个晚上,业务方突然反馈说接口超时严重,紧接着监控平台刷出一堆报错:Lock wait timeo…

作者头像 李华
网站建设 2026/10/13 4:32:46

逻辑回归:二分类入门基石与工程落地全指南

1. 逻辑回归不是“回归”,而是最硬核的分类起点很多人第一次看到“逻辑回归”这四个字,下意识就把它划进“预测连续值”的回归算法阵营里——我带过的不少刚转行的朋友,包括某高校数据科学方向的研究生,在第一次跑通鸢尾花二分类实…

作者头像 李华
网站建设 2026/10/13 4:32:42

H.264码流分析工具实战:一条命令看懂NALU与SPS/PPS

简介:H.264分析工具资源包是一套面向视频编码工程师、开发者和研究人员的码流解析工具,专门用于读取H.264/AVC编码流,展示宏块类型、熵编码状态、量化参数等底层信息,帮助排查编码错误、评估视频质量并优化压缩参数。压缩包共186个…

作者头像 李华
网站建设 2026/10/13 4:32:41

Open Code Review:一种以Git为基座的开放协作范式

1. 项目概述:这不是代码检查,而是一场协作范式的重构“open-code-review”这个词组乍看像一个技术名词,实则是一次开发文化层面的悄然转向。它不指代某个具体工具、平台或开源项目,而是描述一种将代码审查(Code Review…

作者头像 李华
网站建设 2026/10/13 4:31:50

日志数据全链路处理实战:从采集、缓冲到存储查询

日志数据这几年越来越不是“配角”了。业务出问题要查日志,用户反馈异常要查日志,算法特征要圈日志,连审计合规也离不开日志。我在一线做数据平台时,几乎每个季度都要处理日志相关的需求,积累了不少实操经验。这次把日…

作者头像 李华
网站建设 2026/10/13 4:30:19

MySQL CASE WHEN 用法详解:从条件判断到复杂查询优化

你写SQL的时候,是不是经常碰到"要根据某个字段的值,决定展示成什么内容"这种需求?比如查订单状态,数据库里存的是数字0、1、2,但业务方要看"待付款""已发货""已完成"&#xf…

作者头像 李华