Skip to content
Charles Shao
Go back

广告多维报表与漏斗分析实战

–views

当广告主在后台按 campaign × 媒体 × 地域切分花费,或者质问为何看板的结算 UV 与账单对不上时,你会发现有了 列存 OLAP 和 实时数仓分层 只是打好了地基,广告主后台最难的那一块才真正登场。本篇把物化视图与预聚合、漏斗语义、Bitmap 与 HLL 去重、API 加速接到 AdTech 的日常问法上,探讨如何让多维报表快、漏斗与留存准,同时确保 UV 口径对得起结算。

本文是 实时数仓与 OLAP 系列的第 3 篇(报表实战)。 全系列 4 篇:

  1. OLAP · 列式存储原理与 ClickHouse/Doris
  2. 实时数仓 · 分层设计与 Lambda/Kappa 架构
  3. 广告多维报表与漏斗分析实战(本篇)
  4. 时序数据库 InfluxDB 深挖 · 数据模型、TSM 与降采样

一句话定位:报表体验 = 正确的指标语义 × 可命中的预聚合 × 可承受的去重成本;列存解决「扫得动」,物化与 Bitmap 解决「问得起、算得准」。

TL;DR

Table of contents

Open Table of contents

1. 广告多维报表到底在问什么

一张广告主看板表面上只是几张简单的表格与折线图,但其底层却对应着固定模板的复杂分析问法:

指标:花费、曝光、点击、CTR、CVR、UV、平均 eCPM …
维度:日期、计划、创意、媒体、地域、操作系统、流量类型 …
过滤:广告主、时区、结算币种、去无效点击 …

面对这种维度组合爆炸,如果每次请求都直接对亿级明细数据执行任意 GROUP BY,即使是再强的列存引擎,也会在并发下发生查询队列积压与抖动。破局策略其实只有两条:首先是认出高频问法(通常只有一二十种核心维度组合),并为它们建立预聚合或物化视图(Materialized View,MV);其次是针对任意 GROUP BY 的冷路径长尾问法,采取触发限流、降级到异步导出或抽样查询的手段。

2. 预聚合与物化视图:热路径优先

多维报表加速路径。冷路径:明细事实表亿级行 → 即时 GROUP BY 任意维度 → 秒到分钟级吃满集群。热路径:Flink 写入明细到 OLAP → 物化视图/Rollup(campaign×media×hour)→ ADS 专题表(漏斗/留存/UV Bitmap)→ 报表 API P99 < 1s。底部说明用固定问法的稳定延迟换灵活度,把高频组合物化。

生产报表默认走热路径;冷路径留给数据同学的临时 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 依然查询明细表,而优化器会自动将其改写并路由到物化视图,从而让产品与工程的接口契约更加干净。

在落地物化视图时,需要把握以下设计要点:

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 种组合;如果维度继续增加,存储与写入的开销将从线性恶化为指数级。因此,在实务中我们建议:

3. 漏斗分析:有序递减,而不是四个独立计数

广告漏斗:曝光 UV 100万 / PV 120万 → 点击 UV 4.5万、CTR≈3.8% → 落地页 UV 2.8万、到达率≈62% → 转化 UV 3200、CVR≈11.4%。每步是严格递减的 UV,精确去重用 Bitmap,计费侧慎用近似。

漏斗每一步都是「穿过了上一步的用户集合」再求去重基数。

3.1 语义要件

漏斗分析的核心在于有序事件上的递减 UV,它绝不是几个独立 PV 的简单比值。要构建严谨的漏斗语义,必须明确以下要件:

  1. 步骤有序:同一用户必须遵循先曝光、再点击、再落地、最终转化的严格顺序,或者在归因窗口内能够形成闭链。
  2. 窗口定义:无论是会话漏斗、当日漏斗,还是 7 日归因窗,产品层面必须选定一种并将其严格写进口径文档中。
  3. 分析主体:通常按 user_id 或 device_id 进行追踪,这里需要特别注意登录态与设备图谱之间的混淆风险。
  4. 去重粒度:步骤内部的 UV 必须进行去重基数计算,而步骤之间的转化则完全依赖集合运算。

一个典型的错误示范是:在漏斗第一步用「点击 PV / 曝光 PV」来计算 CTR,却在第二步的转化率计算中改用 UV。这种上下游口径未收敛的做法,必然会导致广告主在对账时提出质疑。

3.2 工程实现素描

在工程实现上,漏斗分析通常分为两条路径:

需要强调的是,如果转化与点击之间涉及广告归因(如点击归因或末次曝光归因),漏斗步骤的时间边界必须以归因结果表为准,而绝对不能使用原始转化日志的事件时刻,否则漏斗数据将永远无法与结算域打平。

3.3 漏斗的常见歪楼

歪楼后果纠正
用 PV 当作漏斗步骤CTR 虚高、同用户膨胀步骤一律采用去重基数(UV)
窗口与结算归因窗不一致看板 CVR 与账单结果不一致严格对齐归因文档
步骤时间戳使用 processing-time乱序流下发生漏算采用 event-time 并允许迟到
跨设备未打通却宣称「用户漏斗」同一用户被多次计算明确分析主体是设备还是人
把「可见曝光」与「竞价成功」混为一步算法优化方向跑偏将指标定义拆开并版本化

正如上表所示,漏斗本质上是产品语义,而不是简单的 SQL 语法糖。在动手写查询之前,必须先将其定义固化到口径中心。

4. 精确去重 vs 近似:Bitmap 与 HLL

去重两条路。左:Roaring Bitmap——用户 ID 字典编码 → 按日/维度挂 Bitmap → 交并差支持漏斗留存 → 可复现可对账,适合计费结算 UV。右:HLL——固定小 sketch、可 MERGE、误差约 1–2%、无法还原集合,适合量级趋势不宜结算。

交集问法(漏斗、留存)几乎点名 Bitmap;HLL 擅长误差带内的基数估计。

4.1 Bitmap(Roaring)

在广告场景中,精确去重(Bitmap)是处理结算与合同口径的唯一标准。其核心机制如下:

正因如此,CH 的 AggregateFunction(groupBitmap, …) 与 Bitmap* 函数族,以及 Doris 的 Bitmap 数据类型,构成了广告 UV 去重的常用武器库。

4.2 HLL

与精确去重相对,高精度基数草图(HyperLogLog,HLL)提供了一种近似去重方案:

规则:钱相关、合同相关 → Bitmap/精确去重;体感相关 → HLL 近似去重可。 在指标定义上含糊其辞,必然会在处理客诉时付出十倍的排查成本。

4.3 字典爆炸与基数治理

在引入 Bitmap 之后,位于其前端的「用户 ID → 整数」字典编码往往会率先成为系统瓶颈。当全站设备 ID 达到几十亿规模时,维护一个全局字典是不现实的。业界常见的拆分策略包括:

需要警惕的是,没有基数治理的 Bitmap 方案,往往会在你终于做对 UV 口径的第二天,就因为状态记录过度膨胀而把集群磁盘写满。

5. 留存:cohort 的集合运算

在 AdTech 场景中,N 日留存的本质是 cohort 的集合运算:

在 D0 进入 cohort 的用户集合,有多少出现在 D0+N 的活跃集合里。

将这一语义落地到列存引擎中,计算过程极为清晰:

  1. 提取 D0 转化用户(或首曝用户)的 Bitmap 集合;
  2. 提取之后每日活跃用户(有曝光、打开或回流行为)的 Bitmap 集合;
  3. 通过交集运算得出留存率: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 对外至少应当固定以下契约字段:

通过这些契约,前端可以优雅地展示「数据已更新至 xx:xx」,这比盲目设置 15 秒轮询要有价值得多,同时也大幅减少了对 OLAP 集群的无效击打。

报表的高效运转离不开上游实时数仓的支撑,两者的接合点主要体现在以下四个方面:

7.1 从竞价到报表的指标闭链

一条广告曝光从竞价出价到最终进入多维看板,数据在底层经历了漫长的流转:竞价日志 → 曝光上报 →(可选)可见性校准 → 计费 → DWD 层 → 越靠近 ADS 越产品化。要确保这条链路的正确性,必须在闭链检查时拷问三个核心问题:

  1. 水量校验:竞价成功数与最终曝光数的比例是否落在合理的误差带内?
  2. 金额核对:计费侧的花费总和(SUM)是否与结算域完全一致?
  3. 基数对齐:看板展示的去重基数(UV)是否与计费侧在相同去重规则下保持一致?

在这条长链路中,任何一环试图用近似去重(HLL)来偷懒,都会在广告主核对账单时被无情地揪出来。只有当指标语义、预聚合策略与去重机制严密咬合,报表链路才能真正做到既问得起,又算得准。

参考


–views
Share this post on:

Previous Post
分布式共识机制 · Paxos 与 Raft 图解
Next Post
特征编码:把文字菜谱翻译成秤上的克数