如何通过PostgreSQL的MODE()函数查找分组数据中的众数,实现基础统计查询?
- 内容介绍
- 相关推荐
本文共计1019个文字,预计阅读时间需要5分钟。
PostgreSQL 14及以上版本原生支持使用MODE作为聚合函数(需配合WITHIN GROUP (ORDER BY ...)使用),仅支持单列、非空值和确定性排序。如果使用的是13或更早版本,直接使用MODE会报错ERROR: function mode() does not exist。
在这种情况下,不要慌张,可以通过升级到PostgreSQL 14或更高版本来解决。在多数场景下,使用GROUP BY + COUNT()配合窗口函数就能达到相同的效果,并且更加灵活可控。
- 14+ 中正确写法是:
MODE() WITHIN GROUP (ORDER BY col),不是MODE(col) - 该函数要求
col不能含NULL,否则整个分组返回NULL - 若多个值并列最高频(如 [a,a,b,b,c]),PostgreSQL 只返回排序靠前的那个(依赖
ORDER BY规则),不报错也不警告
用 ROW_NUMBER() + COUNT() 实现兼容所有版本的众数
这是最稳妥、可读性高、且能处理并列众数的写法。核心思路:先按分组和值计数,再对每个分组内按频次降序排号,取 rn = 1 的记录。
SELECT category, val AS mode_val FROM ( SELECT category, val, ROW_NUMBER() OVER (PARTITION BY category ORDER BY COUNT(*) DESC, val) AS rn FROM products WHERE val IS NOT NULL GROUP BY category, val ) t WHERE rn = 1;
-
WHERE val IS NOT NULL必须显式加,否则NULL会被计入并可能成为“众数” -
ORDER BY COUNT(*) DESC, val中的val是为打破频次相同时的不确定性;若业务允许任选一个,可只留COUNT(*) DESC - 若要返回全部并列众数(不止一个),把
ROW_NUMBER()换成RANK(),再改成RANK() = 1
用 ARRAY_AGG() + MODE() 组合在 14+ 中获取多值众数
原生 MODE() 不支持返回多个结果,但你可以先聚合为数组,再用 UNNEST + 子查询模拟“取最高频集合”。不过更实用的做法是:用 ARRAY_AGG(DISTINCT val) FILTER (WHERE ...) 配合频次判断。
但注意——下面这个看似简洁的写法是**错误的**:
-- ❌ 错误:MODE() 不能直接作用于 ARRAY_AGG 结果 SELECT MODE(ARRAY_AGG(val)) FROM products GROUP BY category;
正确路径仍是先统计频次,再用数组聚合筛选:
SELECT category, (ARRAY_AGG(val ORDER BY cnt DESC))[1] AS mode_val FROM ( SELECT category, val, COUNT(*) AS cnt FROM products WHERE val IS NOT NULL GROUP BY category, val ) t GROUP BY category;
-
(ARRAY_AGG(...) [1])取第一个,等价于ROW_NUMBER() = 1的效果,但少一层子查询 - 若想返回全部众数(数组形式),改用:
ARRAY_AGG(val) FILTER (WHERE cnt = MAX(cnt) OVER (PARTITION BY category))—— 这里嵌套了窗口最大值,需两层子查询或 CTE
性能与 NULL 处理:为什么不能跳过 WHERE col IS NOT NULL
在真实业务数据中,NULL 往往占比不小。如果漏掉 IS NOT NULL 过滤,会出现两种隐蔽问题:
- 当某分组中
NULL出现次数最多时,MODE()或自定义逻辑会返回NULL,看起来像“无众数”,实则是脏数据干扰 -
GROUP BY col会把所有NULL归为同一组,导致频次虚高,尤其在低基数列(如状态字段)上极易误导结论 - 索引无法覆盖含
NULL的聚合,全表扫描概率上升;加WHERE col IS NOT NULL后,若该列上有部分索引(CREATE INDEX idx ON t(col) WHERE col IS NOT NULL),可明显提速
众数本身是对“典型值”的刻画,而 NULL 表达的是“未知”或“不适用”,语义上就不该参与统计。这步过滤不是可选项,是建模前提。
本文共计1019个文字,预计阅读时间需要5分钟。
PostgreSQL 14及以上版本原生支持使用MODE作为聚合函数(需配合WITHIN GROUP (ORDER BY ...)使用),仅支持单列、非空值和确定性排序。如果使用的是13或更早版本,直接使用MODE会报错ERROR: function mode() does not exist。
在这种情况下,不要慌张,可以通过升级到PostgreSQL 14或更高版本来解决。在多数场景下,使用GROUP BY + COUNT()配合窗口函数就能达到相同的效果,并且更加灵活可控。
- 14+ 中正确写法是:
MODE() WITHIN GROUP (ORDER BY col),不是MODE(col) - 该函数要求
col不能含NULL,否则整个分组返回NULL - 若多个值并列最高频(如 [a,a,b,b,c]),PostgreSQL 只返回排序靠前的那个(依赖
ORDER BY规则),不报错也不警告
用 ROW_NUMBER() + COUNT() 实现兼容所有版本的众数
这是最稳妥、可读性高、且能处理并列众数的写法。核心思路:先按分组和值计数,再对每个分组内按频次降序排号,取 rn = 1 的记录。
SELECT category, val AS mode_val FROM ( SELECT category, val, ROW_NUMBER() OVER (PARTITION BY category ORDER BY COUNT(*) DESC, val) AS rn FROM products WHERE val IS NOT NULL GROUP BY category, val ) t WHERE rn = 1;
-
WHERE val IS NOT NULL必须显式加,否则NULL会被计入并可能成为“众数” -
ORDER BY COUNT(*) DESC, val中的val是为打破频次相同时的不确定性;若业务允许任选一个,可只留COUNT(*) DESC - 若要返回全部并列众数(不止一个),把
ROW_NUMBER()换成RANK(),再改成RANK() = 1
用 ARRAY_AGG() + MODE() 组合在 14+ 中获取多值众数
原生 MODE() 不支持返回多个结果,但你可以先聚合为数组,再用 UNNEST + 子查询模拟“取最高频集合”。不过更实用的做法是:用 ARRAY_AGG(DISTINCT val) FILTER (WHERE ...) 配合频次判断。
但注意——下面这个看似简洁的写法是**错误的**:
-- ❌ 错误:MODE() 不能直接作用于 ARRAY_AGG 结果 SELECT MODE(ARRAY_AGG(val)) FROM products GROUP BY category;
正确路径仍是先统计频次,再用数组聚合筛选:
SELECT category, (ARRAY_AGG(val ORDER BY cnt DESC))[1] AS mode_val FROM ( SELECT category, val, COUNT(*) AS cnt FROM products WHERE val IS NOT NULL GROUP BY category, val ) t GROUP BY category;
-
(ARRAY_AGG(...) [1])取第一个,等价于ROW_NUMBER() = 1的效果,但少一层子查询 - 若想返回全部众数(数组形式),改用:
ARRAY_AGG(val) FILTER (WHERE cnt = MAX(cnt) OVER (PARTITION BY category))—— 这里嵌套了窗口最大值,需两层子查询或 CTE
性能与 NULL 处理:为什么不能跳过 WHERE col IS NOT NULL
在真实业务数据中,NULL 往往占比不小。如果漏掉 IS NOT NULL 过滤,会出现两种隐蔽问题:
- 当某分组中
NULL出现次数最多时,MODE()或自定义逻辑会返回NULL,看起来像“无众数”,实则是脏数据干扰 -
GROUP BY col会把所有NULL归为同一组,导致频次虚高,尤其在低基数列(如状态字段)上极易误导结论 - 索引无法覆盖含
NULL的聚合,全表扫描概率上升;加WHERE col IS NOT NULL后,若该列上有部分索引(CREATE INDEX idx ON t(col) WHERE col IS NOT NULL),可明显提速
众数本身是对“典型值”的刻画,而 NULL 表达的是“未知”或“不适用”,语义上就不该参与统计。这步过滤不是可选项,是建模前提。

