postgresql不支持count(distinct) over(),因其本质是聚合函数,而over要求窗口函数,二者语义冲突;替代方案常用array_agg+unnest+cardinality,但需注意order by、distinct去重及null处理。

PostgreSQL 不支持 COUNT(DISTINCT) OVER() 的根本原因
因为 COUNT(DISTINCT) 是聚合函数,而 OVER 子句要求的是窗口函数——这两类函数在 SQL 语义层面冲突。PostgreSQL(以及几乎所有主流数据库)不允许把聚合函数嵌套进窗口函数上下文里。你写 COUNT(DISTINCT user_id) OVER (PARTITION BY dept),会直接报错:ERROR: aggregate function calls cannot be nested。
用 array_agg + unnest + cardinality 模拟的实操细节
这是 PostgreSQL 中最常用、语法相对清晰的替代方案,但必须注意三处易错点:
-
array_agg(user_id) OVER (ORDER BY event_time)必须带ORDER BY,否则窗口行为不可控,且结果可能不满足“累计”语义 -
unnest(array_agg(...))后要用SELECT DISTINCT x FROM ... AS x去重,不能漏掉DISTINCT关键字,否则仍是重复计数 -
cardinality()返回数组长度,但若原始数据含NULL,array_agg默认保留NULL,而unnest+DISTINCT会把所有NULL视为相等并只留一个——这符合多数业务预期,但如果你需要排除NULL,得提前加WHERE user_id IS NOT NULL
示例:
SELECT event_time, user_id, cardinality(ARRAY(SELECT DISTINCT x FROM unnest(array_agg(user_id) OVER (ORDER BY event_time)) AS x)) AS cum_distinct_users FROM events;
DISTINCT ON 和窗口函数不是一回事
DISTINCT ON 看起来像“按某列去重”,但它不涉及窗口计算,也不返回统计值,只是从每组中挑一行出来。它和 ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...) 在效果上有时重叠,但机制完全不同:
-
DISTINCT ON (dept_id) dept_id, name, salary要求ORDER BY dept_id, salary DESC,否则哪行被选中是不确定的 - 它不能用来算“每个部门有多少个不同用户”,只能返回“每个部门薪资最高的那个员工”这类单行结果
- 想统计去重数量,还是得回到
array_agg或子查询路线,DISTINCT ON帮不上忙
真正该优先考虑的替代路径:先聚合再关联
多数所谓“需要窗口去重计数”的场景,其实可以拆成两步:先用 GROUP BY 去重聚合,再用 JOIN 或 LATERAL 补回明细。这样既避免窗口膨胀,性能也更可控:
- 如果目标是“每个城市有多少独立用户”,直接
SELECT city, COUNT(DISTINCT user_id) FROM logs GROUP BY city—— 这比任何窗口模拟都快 - 如果必须保留原始行(比如要显示每条日志+对应城市的累计去重用户数),才考虑窗口方案;此时要注意
array_agg随窗口扩大内存占用线性增长,万级窗口就可能 OOM - 小数据量可接受时,CTE 先算出各分组的去重结果,再用
JOIN关联回来,逻辑更直白、调试更方便
窗口函数不是万能胶水,强行套用 array_agg 解法容易在数据量变大后突然卡死,这点常被忽略。









