第 8 章 · 聚合、窗口函数与数组

交互式演示:精确 vs 近似 uniq / -State -Merge / windowFunnel / ARRAY JOIN

uniq 三兄弟:内存占用 vs 误差

滑动「基数」改变去重数据的规模(独立用户数),观察四种算法的内存占用与误差变化。误差为模拟值,与官方误差范围一致。

1,000,000

内存占用对比 (KB)

误差对比 (%)

结论uniqExact 内存随基数线性暴涨; uniqHLL12 永远只占 ~3 KB; uniq(HLL++ 自适应)兼顾速度和精度,是日常 UV 的默认选择。
函数算法内存误差推荐场景
uniqExact全量 hash setO(N)0反作弊、对账
uniqHLL++ 自适应~几十 KB~0.5%日常 UV / DAU 默认
uniqCombined小集合 hash + 大集合 HLL自适应~0.1%中等量、要求高
uniqHLL12标准 HLL 2^12 桶固定 ~3 KB~1.5%极致省内存

-State / -Merge 工作原理动画

点击左边几张分片表的「输出 -State」,看每张表把聚合「中间状态」(HLL 桶的二进制)传给中间区,最后在右边由 uniqMerge 合并成最终 UV。

阶段:等待开始

① 三张分片表

shard1:1000 万行
uid 范围 1~3M
shard2:1200 万行
uid 范围 2M~5M
shard3:800 万行
uid 范围 4M~7M

② 聚合中间状态(uniqState)

类型:AggregateFunction(uniq, UInt64) —— 这是 HLL 桶数组的二进制序列

③ uniqMerge 合并结果

uniqMerge 把多个 HLL state 按位 OR 合并 → 一次估算 → 全局 UV
为什么需要 state? 因为 uniq 不能简单相加: 三个 100 万 UV 的并集 ≠ 300 万。-State 保留 HLL 桶让我们能正确合并; -Merge 把多个状态按位合并并最终估算。

典型应用:AggregatingMergeTree + 物化视图

角色SQL 写法
底表 schemauv AggregateFunction(uniq, UInt64)
物化视图写入SELECT day, uniqState(uid) AS uv ...
查询时合并SELECT day, uniqMerge(uv) FROM ... GROUP BY day

windowFunnel 漏斗匹配步进演示

定义漏斗:view → click → addcart → pay,窗口 30 分钟。点击「步进」,看一个用户的事件流如何被算法逐步匹配。

当前 level
0
最深完成步数
第 1 步时间
已扫事件
0
窗口剩余 (s)

漏斗可视化(基于当前 level)

提示windowFunnel(window_seconds)(ts, cond1, cond2, ...) 会从前往后扫一遍,遇到 cond1 开新链,遇到 cond_{level+1} 且离 cond1 在窗口内就 +1,最终返回最深步数。

SQL 等价写法

SELECT
    windowFunnel(1800)(
        ts,
        event_type='view',
        event_type='click',
        event_type='addcart',
        event_type='pay'
    ) AS level,
    count() AS users
FROM events GROUP BY uid

ARRAY JOIN 行展开前后对比

左边是 5 条原始记录(每条带一个 tags 数组),右边是 ARRAY JOIN 后的样子。点击按钮切换 ARRAY JOINLEFT ARRAY JOIN,看对空数组行的差异。

原始数据 (5 行)

展开结果

核心区别ARRAY JOIN 把空数组所在的行**丢掉**; LEFT ARRAY JOIN 保留它,展开列取类型默认值(如 String 是 '')。

等价 SQL

SELECT id, tag
FROM events
ARRAY JOIN tags AS tag;       -- 空数组的行被丢

SELECT id, tag
FROM events
LEFT ARRAY JOIN tags AS tag;  -- 空数组的行保留,tag = ''