PostgreSQL中MODE()不能用于GROUP BY分组求众数,因其是有序集聚合函数,仅支持单组场景;正确方法是用两层窗口函数:先COUNT(*) OVER(PARTITION BY group_col, value_col)统计频次,再RANK() OVER(PARTITION BY group_col ORDER BY cnt DESC, value_col ASC)取所有并列众数。

PostgreSQL里MODE()不能直接用在GROUP BY后取每组众数
直接写 SELECT group_col, MODE() WITHIN GROUP (ORDER BY value_col) FROM t GROUP BY group_col 语法上不报错,但结果是错的:它会把整个表当做一个隐式分组来算众数,而不是按 group_col 切分后各自计算。你看到的只是“全表众数”被重复输出了 N 次,和分组完全无关。
根本原因在于:MODE() 是一个**有序集聚合函数(ordered-set aggregate)**,它只接受 WITHIN GROUP 子句,不支持与 GROUP BY 协同做“先分组、再在组内排序聚合”的语义。它没有窗口函数那种 PARTITION BY 能力。
所以别被名字误导——MODE() 不是“窗口函数”,也不是“分组内函数”,它只是个单次聚合,仅适用于单组场景(比如全表或子查询已限定范围)。
真正能分组取众数的只有窗口函数组合写法
必须靠两层窗口函数配合:先统计频次,再排序取首行。这是 PostgreSQL 8.0+、MySQL 8.0+、SQL Server 都通用的可靠方案。
-
COUNT(*) OVER (PARTITION BY group_col, value_col):对每个(group_col, value_col)组合计数,得到该值在本组中出现几次 -
ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY cnt DESC, value_col ASC):在每组内按频次降序排,频次相同时用value_col ASC保证顺序确定(避免每次查询结果随机波动) - 最外层
WHERE rn = 1筛出每组频次最高的那个值(注意:只返回一个,即使并列也不多返)
示例 SQL:
WITH freq AS (
SELECT
group_col,
value_col,
COUNT(*) OVER (PARTITION BY group_col, value_col) AS cnt
FROM t
),
ranked AS (
SELECT
group_col,
value_col,
ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY cnt DESC, value_col ASC) AS rn
FROM freq
)
SELECT group_col, value_col AS mode_value
FROM ranked
WHERE rn = 1;
想返回所有并列众数?把ROW_NUMBER()换成RANK()
ROW_NUMBER() 对相同频次的值强制编号为 1,2,3…,所以只拿得到一个;而 RANK() 会给相同频次的值赋予相同名次(比如都是 1),后续跳过对应位数(如 1,1,3…)。这样 WHERE rnk = 1 就能拿到全部最高频的值。
只需改一行:
ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY cnt DESC, value_col ASC)
→
RANK() OVER (PARTITION BY group_col ORDER BY cnt DESC, value_col ASC)
注意:RANK() 不会跳过 NULL 值,但若 value_col 本身含 NULL,它会被计入频次统计(除非你显式 WHERE value_col IS NOT NULL 过滤)。
MODE() 的真实适用场景其实很窄
它只适合以下两种情况:
- 全表众数:例如
SELECT MODE() WITHIN GROUP (ORDER BY status) FROM orders - 子查询已限定单一分组:例如
SELECT MODE() WITHIN GROUP (ORDER BY value_col) FROM t WHERE group_col = 'A'
一旦涉及动态分组(尤其是 GROUP BY 或窗口切分),MODE() 就失效。很多人卡在这一步,是因为误以为函数名带“MODE”就天然支持分组逻辑——它不支持,也没打算支持。
真正容易被忽略的点是:MODE() 对 NULL 值默认忽略(不参与排序也不影响结果),但在窗口写法里,NULL 会被正常计频次、参与排序。要不要排除 NULL,得你自己在 WHERE 或 FILTER 中明确控制。










