当广告主在后台按 campaign × 媒体 × 地域切分花费,或者质问为何看板的结算 UV 与账单对不上时,你会发现有了 列存 OLAP 和 实时数仓分层 只是打好了地基,广告主后台最难的那一块才真正登场。本篇把物化视图与预聚合、漏斗语义、Bitmap 与 HLL 去重、API 加速接到 AdTech 的日常问法上,探讨如何让多维报表快、漏斗与留存准,同时确保 UV 口径对得起结算。
本文是 实时数仓与 OLAP 系列的第 3 篇(报表实战)。 全系列 4 篇:
一句话定位:报表体验 = 正确的指标语义 × 可命中的预聚合 × 可承受的去重成本;列存解决「扫得动」,物化与 Bitmap 解决「问得起、算得准」。
TL;DR
- 多维聚合是广告看板核心:
GROUP BY任意维度组合 +sum(cost)/count/uniq;维度组合爆炸时必须预聚合 / 物化视图。 - 冷路径扫明细只留给探索;热路径走 Rollup/MV → ADS → API,P99 打进 1s。
- 漏斗是有序事件上的递减 UV,不是简单 PV 比值;窗口与步骤定义必须产品化。
- 留存是 cohort × 日偏移的集合运算,Bitmap 交并天然契合。
- 精确去重用 Bitmap(Roaring),近似用 HLL——结算/账单禁止把 HLL 当真相。
- 加速组合拳:物化 + 分区/排序键 + API 结果缓存 +(可选)布隆挡穿透。Flink 负责把 ADS 喂饱。
Table of contents
Open Table of contents
1. 广告多维报表到底在问什么
一张广告主看板表面上只是几张简单的表格与折线图,但其底层却对应着固定模板的复杂分析问法:
指标:花费、曝光、点击、CTR、CVR、UV、平均 eCPM …
维度:日期、计划、创意、媒体、地域、操作系统、流量类型 …
过滤:广告主、时区、结算币种、去无效点击 …
面对这种维度组合爆炸,如果每次请求都直接对亿级明细数据执行任意 GROUP BY,即使是再强的列存引擎,也会在并发下发生查询队列积压与抖动。破局策略其实只有两条:首先是认出高频问法(通常只有一二十种核心维度组合),并为它们建立预聚合或物化视图(Materialized View,MV);其次是针对任意 GROUP BY 的冷路径长尾问法,采取触发限流、降级到异步导出或抽样查询的手段。
2. 预聚合与物化视图:热路径优先
生产报表默认走热路径;冷路径留给数据同学的临时 dig。
2.1 预聚合表(人工 Rollup)
回到刚才那条宽表聚合,最直接的解法是手搓人工 Rollup 表。例如日级维度的聚合:
-- 示意:按计划 × 媒体 × 天
SELECT
event_date, campaign_id, media_id,
sum(cost) AS cost,
count() AS imps,
countIf(is_click) AS clks
FROM dwd_impression
GROUP BY event_date, campaign_id, media_id;
这类预聚合表通常由 Flink 窗口计算产出,或者由 OLAP 引擎定时刷新。其优点在于数据流转完全可控,但缺点同样致命:一旦维度组合爆炸,表数量与维护作业数量将呈指数级增长,最终演变成难以维护的负担。
2.2 物化视图 / Projection
对比前一种方案,ClickHouse(下称 CH)的物化视图与 Projection,以及 Doris 的同步或异步物化视图,能够把高频的 GROUP BY 逻辑交给引擎在底层增量维护。此时,业务端的报表 SQL 依然查询明细表,而优化器会自动将其改写并路由到物化视图,从而让产品与工程的接口契约更加干净。
在落地物化视图时,需要把握以下设计要点:
- 对齐排序键与分区键:这也解释了为什么开篇讲的排序键必须先于压缩算法定下来。只有对齐键值,才能让写入与查询共享数据局部性,避免不必要的 I/O。
- 指标必须具备可增量性:
sum与count天然友好,而精确去重基数(UV)则必须依赖 Bitmap 状态记录(详见 §4)。 - 指标定义版本化:当发生口径漂移或业务变更时,务必新建物化视图,经过速度层与批层双跑对比无误后,再将流量切换至新版本。
2.3 ROLLUP / CUBE:小心组合爆炸
SQL 语法中的 CUBE 与 ROLLUP 能够一次性算出多个维度的小计,虽然写起来极为简便,却极易导致中间结果膨胀,甚至引发 compaction 积压与 merge 风暴:
SELECT campaign_id, media_id, os, sum(cost)
FROM dwd_impression
WHERE event_date = today()
GROUP BY CUBE (campaign_id, media_id, os);
仅仅是三维 CUBE 就会产生 8 种组合;如果维度继续增加,存储与写入的开销将从线性恶化为指数级。因此,在实务中我们建议:
- 产品看板需要的小计应当显式建立物化视图,而不是放任对明细表发起任意
CUBE运算。 - 前端展示的「合计行」优先在报表 API 层对已有聚合结果再求和(针对可加性指标),从而避免每次查询都向底层下推
CUBE。 - 针对不可加指标(如 UV、CTR),绝对不能对子节点进行简单相加,必须回到 Bitmap 集合运算或原始定义式进行重算。
3. 漏斗分析:有序递减,而不是四个独立计数
漏斗每一步都是「穿过了上一步的用户集合」再求去重基数。
3.1 语义要件
漏斗分析的核心在于有序事件上的递减 UV,它绝不是几个独立 PV 的简单比值。要构建严谨的漏斗语义,必须明确以下要件:
- 步骤有序:同一用户必须遵循先曝光、再点击、再落地、最终转化的严格顺序,或者在归因窗口内能够形成闭链。
- 窗口定义:无论是会话漏斗、当日漏斗,还是 7 日归因窗,产品层面必须选定一种并将其严格写进口径文档中。
- 分析主体:通常按
user_id或device_id进行追踪,这里需要特别注意登录态与设备图谱之间的混淆风险。 - 去重粒度:步骤内部的 UV 必须进行去重基数计算,而步骤之间的转化则完全依赖集合运算。
一个典型的错误示范是:在漏斗第一步用「点击 PV / 曝光 PV」来计算 CTR,却在第二步的转化率计算中改用 UV。这种上下游口径未收敛的做法,必然会导致广告主在对账时提出质疑。
3.2 工程实现素描
在工程实现上,漏斗分析通常分为两条路径:
- 明细路径:将用户事件按分片键排序并划分窗口,利用状态机(如 Flink CEP 或窗口 Session)逐个匹配步骤。这种方式延迟极低,非常适合实时漏斗大屏。
- Bitmap 路径:每日针对每个步骤生成一张用户 Bitmap 集合,漏斗的计算转化为逐步执行交集运算后再求基数。这种方式不仅适合日报与多维看板,而且完全可重放、可复现。
需要强调的是,如果转化与点击之间涉及广告归因(如点击归因或末次曝光归因),漏斗步骤的时间边界必须以归因结果表为准,而绝对不能使用原始转化日志的事件时刻,否则漏斗数据将永远无法与结算域打平。
3.3 漏斗的常见歪楼
| 歪楼 | 后果 | 纠正 |
|---|---|---|
| 用 PV 当作漏斗步骤 | CTR 虚高、同用户膨胀 | 步骤一律采用去重基数(UV) |
| 窗口与结算归因窗不一致 | 看板 CVR 与账单结果不一致 | 严格对齐归因文档 |
| 步骤时间戳使用 processing-time | 乱序流下发生漏算 | 采用 event-time 并允许迟到 |
| 跨设备未打通却宣称「用户漏斗」 | 同一用户被多次计算 | 明确分析主体是设备还是人 |
| 把「可见曝光」与「竞价成功」混为一步 | 算法优化方向跑偏 | 将指标定义拆开并版本化 |
正如上表所示,漏斗本质上是产品语义,而不是简单的 SQL 语法糖。在动手写查询之前,必须先将其定义固化到口径中心。
4. 精确去重 vs 近似:Bitmap 与 HLL
交集问法(漏斗、留存)几乎点名 Bitmap;HLL 擅长误差带内的基数估计。
4.1 Bitmap(Roaring)
在广告场景中,精确去重(Bitmap)是处理结算与合同口径的唯一标准。其核心机制如下:
- 首先将用户 ID 映射为连续整数,通常通过字典编码(Dictionary)或哈希到 64-bit 后再进行二次编码。
- 接着按
date × 维度将状态记录为 Bitmap 列式连续落盘。 - 在计算留存时,直接执行
bitmapAnd(d0, d7);在计算漏斗时,则逐步进行bitmapAnd交集运算。 - 由于 Bitmap 的存储体积与基数分布强相关,面对高基数维度时必须严格控制粒度,例如优先按天、按计划聚合,极力避免对所有维度进行笛卡尔积展开。
正因如此,CH 的 AggregateFunction(groupBitmap, …) 与 Bitmap* 函数族,以及 Doris 的 Bitmap 数据类型,构成了广告 UV 去重的常用武器库。
4.2 HLL
与精确去重相对,高精度基数草图(HyperLogLog,HLL)提供了一种近似去重方案:
- 它占用固定内存,支持状态合并(MERGE),非常适合超高基数的宏观趋势分析。
- 但其致命弱点在于无法干净地表达「集合交」,因此难以用于漏斗与留存计算。
- HLL 约 1–2% 的误差对于老板大屏或许可以接受,但对于账单 UV 和结算 DAU 则是绝对不可接受的。
规则:钱相关、合同相关 → Bitmap/精确去重;体感相关 → HLL 近似去重可。 在指标定义上含糊其辞,必然会在处理客诉时付出十倍的排查成本。
4.3 字典爆炸与基数治理
在引入 Bitmap 之后,位于其前端的「用户 ID → 整数」字典编码往往会率先成为系统瓶颈。当全站设备 ID 达到几十亿规模时,维护一个全局字典是不现实的。业界常见的拆分策略包括:
- 按天局部编号:仅服务于当日的交并运算;若需计算跨日留存,则采用稳定的 hash 算法映射到 64-bit 后再构建 Roaring Bitmap(容忍极低概率的碰撞),或者使用分段字典。
- 按广告主分片字典:因为广告主后台查询天然带有租户过滤条件,按广告主分片能有效缩小字典加载范围。
- 冷用户淘汰:将超长尾设备排除在精细 Bitmap 之外,仅让其进入 HLL 趋势统计。
需要警惕的是,没有基数治理的 Bitmap 方案,往往会在你终于做对 UV 口径的第二天,就因为状态记录过度膨胀而把集群磁盘写满。
5. 留存:cohort 的集合运算
在 AdTech 场景中,N 日留存的本质是 cohort 的集合运算:
在 D0 进入 cohort 的用户集合,有多少出现在 D0+N 的活跃集合里。
将这一语义落地到列存引擎中,计算过程极为清晰:
- 提取 D0 转化用户(或首曝用户)的 Bitmap 集合;
- 提取之后每日活跃用户(有曝光、打开或回流行为)的 Bitmap 集合;
- 通过交集运算得出留存率:
retain = |B0 ∩ BN| / |B0|。
在构建预聚合表时,应当按 cohort_date × N × 维度 提前算好分子与分母,让前端看板直接查询越靠近 ADS 越产品化的结果表。千万不要每次查询都从 ODS 明细重放 30 天的事件流,除非你是在离线环境进行口径漂移的研究。
6. 报表 API 加速清单
Serving 层的职责绝不仅限于「把 SQL 丢给 OLAP 执行」,而是需要一套完整的加速组合拳:
| 手段 | 作用 | 注意 |
|---|---|---|
| 物化视图 / Rollup | 降低列式存储的扫描字节量 | 优先覆盖高频维度组合 |
| 分区键 + 排序键 | 实现数据裁剪与共享局部性 | 必须与高频过滤条件严格对齐 |
| API 结果缓存 | 拦截重复的看板刷新请求 | 缓存键必须包含口径版本与时区 |
| 布隆过滤器 / 空缓存 | 穿透拦截,挡住乱造的 ID | 详见 布隆过滤器 |
| 查询队列隔离 | 将看板热路径与临时 dig 分离 | 避免任意 GROUP BY 的冷路径打死集群 |
| 触发限流与异步导出 | 兜底长尾的大规模查询 | 超过阈值自动降级为异步任务系统 |
需要特别强调的是,API 缓存层必须带上口径版本号。当底层发版切换物化视图时,旧缓存必须随之失效,否则广告主就会在刷新看板时遭遇口径漂移,看到速度层与批层结果不一致的诡异现象。另外,布隆过滤器只能用来挡住不存在的 ID,绝不能越界去充当 UV 计数的真相。
6.1 接口契约建议
为了保证端到端的透明度,报表 API 对外至少应当固定以下契约字段:
metric_version/dict_version:显式声明当前使用的口径与字典版本;data_as_of:标明数据一致性快照稳定到的事件时间上界;partial:标识当前时间窗内的指标是否仍可能因迟到数据而发生变化;query_path:记录查询命中的是hit_mv、hit_cache还是scan_detail(此字段主要用于排障,不必暴露给广告主)。
通过这些契约,前端可以优雅地展示「数据已更新至 xx:xx」,这比盲目设置 15 秒轮询要有价值得多,同时也大幅减少了对 OLAP 集群的无效击打。
7. 与实时仓、Flink 的接合点
报表的高效运转离不开上游实时数仓的支撑,两者的接合点主要体现在以下四个方面:
- 写入机制:提到清洗、对齐与 Exactly-Once 写出时,Flink 必须以 Exactly-Once 语义或幂等 sink 将数据写入 DWD 层,随后再驱动物化视图或下游的 ADS 层。
- 迟到数据处理:流处理的 watermark 与业务归因窗共同决定了「报表指标是否还会变化」,因此产品端必须透出「数据稳定时间」。
- 重放与补数:正如上一篇提到的 Kappa 单逻辑重放,一旦触发回刷修数,落到本篇就是 ADS 覆盖写之后,对应的 API 缓存必须整层失效。
- 日终对账:每天日终冻结分区后,必须使用同一份 Bitmap 与指标定义与结算域进行自动化对比。任何 diff 都应直接触发告警,而不是依赖人工导出 Excel 核对。
7.1 从竞价到报表的指标闭链
一条广告曝光从竞价出价到最终进入多维看板,数据在底层经历了漫长的流转:竞价日志 → 曝光上报 →(可选)可见性校准 → 计费 → DWD 层 → 越靠近 ADS 越产品化。要确保这条链路的正确性,必须在闭链检查时拷问三个核心问题:
- 水量校验:竞价成功数与最终曝光数的比例是否落在合理的误差带内?
- 金额核对:计费侧的花费总和(
SUM)是否与结算域完全一致? - 基数对齐:看板展示的去重基数(UV)是否与计费侧在相同去重规则下保持一致?
在这条长链路中,任何一环试图用近似去重(HLL)来偷懒,都会在广告主核对账单时被无情地揪出来。只有当指标语义、预聚合策略与去重机制严密咬合,报表链路才能真正做到既问得起,又算得准。
参考
- ClickHouse Docs. Functions for Working with Bitmaps:Bitmap 与漏斗/留存实现入口。
- Apache Doris Docs. Bitmap:Doris Bitmap 类型与聚合。
- ClickHouse. Materialized Views:增量物化与投影思路。
- 本系列. 列式存储 · 实时数仓分层。
- Flink 深挖开篇. 流处理模型与运行时架构:实时写入侧的作业心智。