COUNT(DISTINCT) 卡在99%因数据倾斜:高频值(如'unknown'、NULL)全路由至单节点,导致该reducer负载过重、OOM或失败;常见倾斜字段包括含大量未登录用户的user_id、空page_url及区域集中region_code。

COUNT(DISTINCT) 为什么一跑就卡在 99%?
因为 COUNT(DISTINCT) 在底层必须把所有要计数的值(比如 user_id)按 key 分发到同一个 reducer 或 task,才能确保“同一个值只算一次”。一旦某个值高频出现(如 user_id = 'unknown' 占 40% 行,或大量 NULL),它就会被全打到单个节点上——其他 reducer 早干完了,就剩它在死扛几千万条记录,任务进度卡住、内存爆 OOM、甚至直接失败。
哪些字段最容易触发倾斜?
不是数据量大才倾斜,而是分布不均才倾斜。常见高危场景包括:
-
user_id字段含大量未登录用户(统一填'guest'或NULL) - 埋点日志中
page_url为空或固定兜底值(如'/'、'404') - 业务主键设计缺陷:用
region_code+date拼接,但某 region 占全量 80% -
COUNT(DISTINCT CONCAT(a, b))这类手动拼接,若a高度集中,拼接后仍集中
绕过倾斜的四种实操方案
别硬刚精确去重,先看业务是否允许妥协,再选路径:
- **近似计数优先**:用
APPROX_COUNT_DISTINCT(user_id)(Spark/Hive/Trino 支持),误差率默认 0.01,性能提升 5–10 倍,且天然支持窗口函数 - **两阶段聚合**:先按
user_id分桶加盐(如CONCAT(user_id, '_', FLOOR(RAND() * 100))),GROUP BY 盐值,再对盐值组去重计数;适合 Hive/Spark SQL - **NULL/特殊值单独处理**:把倾斜值拎出来,
WHERE user_id IS NOT NULL跑主逻辑,再UNION ALL SELECT 1补上 null 行数 - **预聚合替代**:如果每天只需独立用户数,就在 ETL 入仓时建宽表,存
dt+distinct_user_set(用COLLECT_SET),查询时SIZE(distinct_user_set)—— 避免每次重算
特别注意:别在窗口里写 COUNT(DISTINCT)
COUNT(DISTINCT x) OVER (PARTITION BY y) 在绝大多数引擎里要么报错(MySQL),要么强制广播全量数据(Spark SQL),极易 OOM。真有这类需求,90% 场景可退化为:COLLECT_SET(x) OVER (PARTITION BY y ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) + SIZE(),但必须确认 x 是有限枚举(如状态码、渠道 ID),否则集合膨胀不可控。

















