count distinct 不能直接用于窗口函数,因sql标准禁止在over()中使用distinct聚合;替代方案包括:用dense_rank+max模拟累计uv、关联子查询计算滚动uv、或使用approx_count_distinct等近似函数。

COUNT DISTINCT 不能直接用在窗口函数里
SQL 标准中,COUNT(DISTINCT ...) 是聚合函数,而窗口函数(如 OVER())只接受**非 DISTINCT 的聚合函数**或纯标量函数。所以写成 COUNT(DISTINCT user_id) OVER (PARTITION BY date) 会报错,常见错误信息是:ERROR: DISTINCT is not supported with window functions(PostgreSQL)、Invalid use of aggregate function(MySQL 8.0.20+ 窗口模式下同样拒绝)。
本质原因是:窗口函数需要逐行输出结果,但 DISTINCT 需要先去重再计数,二者语义冲突——你无法“对当前窗口内已看到的 user_id 去重计数”,除非显式定义顺序和累积逻辑。
替代方案:用 DENSE_RANK + MAX 模拟累计独立访客
如果目标是“截至某天的累计独立访客数”(比如日活 DAU 的累加),可用 DENSE_RANK() 配合 MAX() 窗口实现:
SELECT
date,
user_id,
MAX(rnk) OVER (PARTITION BY user_id ORDER BY date) AS first_seen_rank,
COUNT(*) FILTER (WHERE first_seen_rank = 1)
OVER (ORDER BY date ROWS UNBOUNDED PRECEDING) AS cumu_uv
FROM (
SELECT date, user_id,
DENSE_RANK() OVER (PARTITION BY user_id ORDER BY date) AS rnk
FROM events
) t;
说明:先给每个 user_id 按 date 排序打上首次出现的标记(rnk = 1),再用窗口 COUNT(*) FILTER 统计“到目前为止有多少个首次出现的用户”。这等价于累计 UV。
注意点:
-
FILTER (WHERE ...)是 PostgreSQL/Redshift 语法;MySQL 不支持,需改用SUM(IF(..., 1, 0)) - 此法只适用于“按时间顺序累计”,不适用于任意窗口分组(如“过去7天独立访客”)
计算滚动窗口独立访客:用 LATERAL 或子查询关联
若需“每个日期对应的过去7天独立访客数”,窗口函数无解,必须用关联子查询或 LATERAL(PostgreSQL):
SELECT e1.date, (SELECT COUNT(DISTINCT e2.user_id) FROM events e2 WHERE e2.date BETWEEN e1.date - INTERVAL '6 days' AND e1.date) AS uv_7d FROM (SELECT DISTINCT date FROM events) e1;
性能关键点:
- 确保
(date, user_id)有联合索引,否则全表扫描代价极高 - BigQuery / Snowflake 可用
ARRAY_AGG(DISTINCT user_id)+ARRAY_LENGTH替代,减少 shuffle - ClickHouse 推荐用
uniqCombined(user_id)聚合函数,比COUNT(DISTINCT)内存更友好
真正想用窗口函数?试试 APPROX_COUNT_DISTINCT
部分引擎提供近似去重函数,支持窗口语法:
- BigQuery:
APPROX_COUNT_DISTINCT(user_id) OVER (PARTITION BY date)✅ - Trino/Presto:
approx_distinct(user_id) OVER (PARTITION BY date)✅ - Spark SQL:
approx_count_distinct(user_id) OVER (PARTITION BY date)✅
误差率通常
独立访客统计真正难的不是语法,而是“去重粒度”的定义:是按 user_id?设备 ID?还是登录态下的 session_id?选错字段,后面所有 COUNT 都白算。










