在关系数据库的标准语言 SQL 中,SELECT 语句承担着从基本表或视图中检索数据的核心职责。当查询需求从"逐行取出满足条件的记录"升级为"按某一列或某几列的值把记录归并成若干小组,再对每个小组做整体统计"时,就需要引入分组查询机制。分组查询的正式术语是分组聚集,它由三个彼此关联又各自独立的概念共同构成,分别是分组依据列、聚集函数以及专门作用于分组结果之上的过滤子句。
分组依据列在 SQL 中由 GROUP BY 子句声明。GROUP BY 子句的教材标准定义是:将查询结果中,在指定分组列上取值相同的所有元组划归为同一个分组,最终每个分组输出一行汇总结果。这里必须强调"每个分组输出一行"这一关键语义——GROUP BY 执行之后,结果集中的行数由分组列上不同取值的个数决定,而不再等于原始表满足条件的行数。例如某销售表包含一百条销售记录,若按部门号分组且全表只有五个部门,那么分组后的结果集就只有五行,每行代表一个部门。
聚集函数,又称聚合函数或统计函数,是对一组值执行计算并返回单一值的函数。SQL 标准规定了五类最常用的聚集函数,分别是计数函数 COUNT、求和函数 SUM、平均值函数 AVG、最大值函数 MAX 与最小值函数 MIN。聚集函数与普通标量函数的本质区别在于其作用对象:普通函数逐行处理单个元组的字段值,而聚集函数以"一个分组内所有行的某一列"作为输入,压缩输出为一个标量。聚集函数通常忽略空值,COUNT 函数在针对具体列统计时也会跳过空值,这一细节在后续常见误区部分会专门展开。
HAVING 子句则是对分组之后的各组进行筛选的子句。它的正式定义是:在 GROUP BY 分组完成后,对各个分组按照分组条件进行过滤,只保留满足指定条件的分组。HAVING 与 WHERE 虽然都承担"过滤"职能,但二者所处的逻辑阶段完全不同——WHERE 在分组之前逐行过滤原始元组,HAVING 在分组之后逐组过滤聚合结果。这一先后关系是整个分组查询知识体系的枢纽,几乎所有软考真题的挖坑点都围绕它展开,本文将在原理机制部分做彻底剖析。
从考试命题的角度看,分组查询的三个概念常常被打包成一条完整的 SQL 语句来考查,考生需要同时看清分组依据列、聚集函数选择与分组过滤条件三者之间的关系。一个常见的综合性需求是"统计每个部门中月薪超过三千元的员工人数,且只列出人数超过十人的部门",这句话里同时包含了逐行过滤、分组统计与分组后筛选三个层次,对应 WHERE、GROUP BY 加 COUNT、HAVING 三个子句的协同配合,缺一不可。能够把一个自然语言需求拆解为这三个层次,是判断考生是否真正掌握分组查询的分水岭。
需要特别澄清的是,HAVING 子句并非必须与 GROUP BY 同时出现。当 HAVING 单独出现而不伴随 GROUP BY 时,整个查询结果被视为一个单一的隐式分组,此时 HAVING 的条件表达式中若出现聚集函数,其作用域便是全表所有满足 WHERE 条件的行。不过在实际考试中,命题人几乎总是将二者捆绑考查,因为只有同时出现才能体现"分组后筛选"的完整语义,考生应把 GROUP BY 与 HAVING 的配对使用作为默认认知。
绝大多数考生在初学 SQL 时都会产生一个根深蒂固的误解:以为查询语句是按照书写顺序执行的,即先 SELECT 后 FROM 再 WHERE。事实恰恰相反。数据库系统对一条 SELECT 语句的处理遵循一套固定的逻辑执行顺序,这套顺序在 SQL 标准中被明确定义,也是各大关系型数据库优化器统一遵循的语义基准。标准的逻辑执行顺序依次为:FROM 子句(确定数据来源并做表连接)到 WHERE 子句(逐行过滤)到 GROUP BY 子句(分组)到 HAVING 子句(分组过滤)到 SELECT 子句(投影与计算)到 ORDER BY 子句(排序)。
这套顺序最反直觉之处在于,SELECT 子句几乎排在最后才被执行,而聚集函数的计算发生在 SELECT 阶段。理解了这一点,许多看似诡异的限制就豁然开朗:为什么 WHERE 子句里不能使用聚集函数?因为 WHERE 执行时分组尚未发生,聚集函数还没有可供统计的分组对象;为什么 SELECT 子句里不能出现未经 GROUP BY 声明的非聚集列?因为分组完成后每个分组只保留一行的位置,非分组列在每个分组内部存在多个不同取值,系统无法确定到底取哪一个,投影阶段自然无从下手。
在数据库引擎内部,GROUP BY 的分组动作有两种典型的物理实现策略。第一种是哈希分组。引擎为分组列建立哈希表,遍历扫描到的每一行,根据分组列的取值计算哈希值,将行归入对应的哈希桶,同一分组列取值的所有行最终落入同一个桶内,扫描结束后每个桶即为一个分组。哈希分组的优势在于不需要预先排序,时间复杂度接近线性,适合分组基数较小、内存充足的场景。
第二种是排序分组。引擎先按分组列对输入行进行排序,使取值相同的行在物理上连续排列,然后顺序扫描,每当分组列的取值发生变化时,便意味着上一个分组结束、下一个分组开始。排序分组在分组基数较大或引擎自身排序能力很强的场景下反而更具优势,也便于同时满足 ORDER BY 的排序需求。无论引擎采用哪种策略,对外呈现的语义完全一致:每个分组只生成一行汇总结果。考生理解这一层,是为了在原理题中不被"分组是否破坏原表顺序"这类干扰项带偏——分组结果的顺序在标准语义中是不保证的,除非显式使用 ORDER BY。
聚合函数在分组上的执行可以理解为两阶段:第一阶段由 GROUP BY 把元组切分成互不相交的集合,第二阶段由 SELECT 中的聚合函数对每个集合独立计算,产生一个标量并写入对应分组的输出行。因此,聚合函数的计算次数等于分组的个数,而非原始表的行数。以 SUM 为例,引擎对每个分组内的目标列逐值累加,跳过其中的空值;若某分组内该列全部为空,则 SUM 的结果为空值而非零,这是命题人偶尔会设置的冷门陷阱。
COUNT 函数的空值语义则更为精细,需要区分两种写法。COUNT 星号统计的是行的总数,只要该行存在便计入,与任何具体列是否为空无关;COUNT 具体列名则统计该列上非空取值的个数,遇到空值会跳过。当分组列本身存在空值时,SQL 标准规定所有分组列为空的行被归入同一个分组,即空值在分组语境下被视作相等的取值,这与普通比较运算中空值既不等于任何值也不等于自身的特殊语义形成鲜明对照。这个反差是软考命题人反复利用的知识缝隙。
聚集函数按其统计语义可分为计数类、求和类、均值类与极值类四组,每组都有明确的适用列类型边界。COUNT 适用于任意类型的列,因为它只关心取值是否存在,不关心取值本身能否参与算术运算。SUM 与 AVG 仅适用于数值类型的列,对字符型列求和或求平均在语义上无意义,数据库会报错。MAX 与 MIN 的适用范围最宽,既适用于数值列,也适用于字符列和日期时间列——对字符列求最大最小值时按字符集的编码序或字典序比较,对日期列则按时间先后比较。考生需要精准记忆这一适用边界,命题人常以"对字符列使用 SUM 是否合法"之类的问题检验。
从更宏观的视角看,聚集函数还可以分为标量聚集与向量聚集两个层次。标量聚集面向全表或单个分组,返回单一标量值,是 SELECT 分组查询中最常见的形式。向量聚集则是窗口函数的思想,它在不合并行的前提下为每一行计算一个基于其所在"窗口"的聚集值,属于 SQL 较新的扩展能力。软考数据库系统工程师的考纲范围基本停留在标量聚集层面,但认识向量聚集的概念有助于在原理题中区分"分组后行数减少"与"窗口计算后行数不变"两种截然不同的行为。
分组查询最基础的形态是单列分组,即 GROUP BY 后面只跟一个分组列,按该列的唯一取值把表切分成若干组。多列分组则是 GROUP BY 后面跟两个或更多列,此时分组依据是这些列取值的有序组合——只有所有分组列取值都相同的行才属于同一分组。多列分组的语义等价于按这些列构成的复合键去重,其分组个数不超过各列不同取值数的乘积。真题中常给出"按部门号与商品编号同时分组"的需求,考察考生对组合分组语义的理解。
分组查询几乎总是与排序配合使用。当查询同时出现 GROUP BY 与 ORDER BY 时,ORDER BY 只能引用分组列或聚集函数的结果列,因为这两类值在每个分组输出行中都是唯一的、确定的。若 ORDER BY 引用了未分组的普通列,则因该列在每个分组内的取值不唯一而触发错误。此外,ORDER BY 还可以直接引用聚集函数在 SELECT 中的别名,对分组结果按统计值的大小升序或降序排列,这是"查询各部门平均销售额并按平均销售额从高到低排序"一类经典需求的规范写法。
HAVING 子句的条件表达式中可以出现两类元素:一是分组列本身,二是包含聚集函数的表达式。因为分组完成后,分组列的取值在组内是唯一的,聚集函数也已经在组上计算出确定值,这两类信息都具备被 HAVING 逐组判断的条件。与之相对,HAVING 的条件表达式中不能引用既非分组列也非聚集函数的普通列,这同样是因为普通列在组内取值不唯一,无法作为分组级别的判断依据。
从表达能力的角度审视,凡是用 HAVING 表达的分组条件,其中涉及分组列的部分理论上可以改写为 WHERE 条件,从而在分组前就完成过滤、减少参与分组的行数,通常更为高效。但涉及聚集函数的条件
本篇完!