在数据分析与业务报表开发中,经常会遇到这样的需求:按照某个维度(例如部门、地区、日期)进行分组,然后在每个分组内部找出出现次数最多的那个值,也就是统计学里的众数。很多初学者会尝试直接用MAX配合GROUP BY,但很快发现MAX只能取数值最大,并不能保留原行的其他字段,因此无法得到我们想要的众数结果。要实现分组内众数统计,核心做法是先统计每个分组中各个取值的出现频次,再从中挑出频次最高的记录。

一、基础思路:COUNT统计频次再筛选
最直观的实现方式是,先通过GROUP BY对分组字段和目标字段同时聚合,使用COUNT得到每个取值在组内的出现次数。此时我们得到了一张中间表,包含分组键、取值和频次三列。接下来只需要找出每个分组里频次最大的那几条记录即可。
以员工表emp为例,表中有dept(部门)和city(工作城市)两个字段,我们想统计每个部门最常见的城市。下面这段MySQL 8之前兼容的写法,使用派生表先算频次,再关联出最大频次:
SELECT t.dept, t.city, t.cnt
FROM (
SELECT dept, city, COUNT(*) AS cnt
FROM emp
GROUP BY dept, city
) t
JOIN (
SELECT dept, MAX(cnt) AS max_cnt
FROM (
SELECT dept, city, COUNT(*) AS cnt
FROM emp
GROUP BY dept, city
) x
GROUP BY dept
) m ON t.dept = m.dept AND t.cnt = m.max_cnt;
上面的SQL中,子查询x先算出每个部门每个城市的人数,外层m按部门取最大人数,最后通过JOIN把频次等于最大值的所有记录选出来。如果某个部门有两个城市并列第一,这段查询会返回两行,这属于合理的并列众数情况。
这种写法的优点是逻辑清晰、兼容性好,几乎所有关系型数据库都能运行;缺点是嵌套层数多,在大数据量下派生表重复扫描原表,性能一般。可以通过把频次统计结果写成临时表来优化。
二、使用窗口函数简化取最大频次
在支持窗口函数的数据库(MySQL 8+、PostgreSQL、SQL Server、Oracle)中,我们可以用ROW_NUMBER或RANK给组内记录按频次排序,从而大幅简化语句。RANK会保留并列第一,ROW_NUMBER则只取其中一个。
以下PostgreSQL示例用RANK实现分组众数,并自动处理并列:
SELECT dept, city, cnt
FROM (
SELECT
dept,
city,
COUNT(*) AS cnt,
RANK() OVER (PARTITION BY dept ORDER BY COUNT(*) DESC) AS rk
FROM emp
GROUP BY dept, city
) s
WHERE rk = 1;
内部查询在GROUP BY之后,利用RANK() OVER按部门分区、按计数降序排名。rk等于1的就是该部门出现最多的城市。若只想要一个结果,把RANK换成ROW_NUMBER,并可在ORDER BY中加次级排序规则,例如城市名字典序,保证结果稳定。
窗口函数写法不仅代码短,而且大多数优化器只需扫描一次基表再做窗口计算,效率优于多轮JOIN派生表。不过要注意,COUNT(*)在窗口排序前已经聚合,因此不会出现行级膨胀。
三、空值与数据质量的影响
实际业务中,目标字段可能存在NULL。SQL里GROUP BY会将NULL单独归为一组,COUNT(*)也会把NULL行算进去,这可能导致NULL成为众数而掩盖真实取值。若业务上NULL无意义,应在统计前过滤。
示例中加入WHERE city IS NOT NULL来排除干扰:
SELECT dept, city, cnt
FROM (
SELECT
dept,
city,
COUNT(*) AS cnt,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY COUNT(*) DESC) AS rn
FROM emp
WHERE city IS NOT NULL
GROUP BY dept, city
) s
WHERE rn = 1;
该语句先剔除空城市,再取每部门最频繁的非空城市。如果保留NULL且想单独看,也可不写过滤,但需在报表注明NULL分组含义,避免解读偏差。
此外,当数据量极大时,建议对分组键建立复合索引(如dept, city),能让GROUP BY走索引有序扫描,减少临时文件排序开销。若只需近似众数,部分数据库提供近似聚合扩展,可进一步提速。
四、与其他聚合方式的对比
有人会问,能否用MODE() WITHIN GROUP直接求众数?的确,PostgreSQL提供了mode()有序集聚合函数,但它只能返回单一值且不支持并列,也不直观展示频次。相比之下,COUNT加ORDER BY的方案既能拿到值也能拿到次数,更灵活。
我们用表格总结几种方式差异:
| 方法 | 是否支持并列 | 能否看频次 | 兼容性 |
|---|---|---|---|
| COUNT加JOIN派生表 | 是 | 是 | 所有数据库 |
| COUNT加窗口RANK | 是 | 是 | 需支持窗口函数 |
| MODE()有序集 | 否 | 否 | 部分数据库 |
从可维护性和信息丰富度看,COUNT配合ORDER BY取最大频次是通用且透明的做法。团队新人也能通过中间结果理解逻辑,不必依赖特定数据库黑盒函数。
综上,计算SQL分组内众数并不复杂,把握住先计数、再按组取最大频次这两步,就能用标准语法在任意环境落地。遇到性能瓶颈时,再从索引和近似计算层面做针对性优化即可。
SQLgroup_by_modeCOUNT修改时间:2026-08-11 02:03:31