有了 列存 OLAP 和 实时数仓分层,广告主后台最难的那一块才真正登场:多维报表要快,漏斗/留存要准,UV 还要对得起结算。本篇把「物化与预聚合、漏斗语义、Bitmap/HLL 去重、API 加速」接到 AdTech 日常问法上,作为本系列收官。
本文是实时数仓与 OLAP 系列的第 3 篇(收官 · 报表实战)。 全系列 3 篇:
- OLAP(开篇)· 列式存储原理与 ClickHouse/Doris
- 实时数仓 · 分层设计与 Lambda/Kappa 架构
- 广告多维报表与漏斗分析实战(本篇)
一句话定位:报表体验 = 正确的指标语义 × 可命中的预聚合 × 可承受的去重成本;列存解决「扫得动」,物化与 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,再强的列存也会在并发下抖动。策略只有两条:
- 认出高频问法(往往就一二十种维度组合),为它们建预聚合/物化;
- 长尾问法限流、降级到异步导出或抽样。
2. 预聚合与物化视图:热路径优先
生产报表默认走热路径;冷路径留给数据同学的临时 dig。
2.1 预聚合表(人工 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 物化视图、Projection,Doris 同步/异步物化视图,把「常见 GROUP BY」交给引擎增量维护。报表 SQL 仍写明细表,优化器改写到 MV——产品与工程接口更干净。
设计要点:
- 对齐排序/分区键,让写入与查询共享局部性;
- 指标要可增量(sum/count 友好;精确 uniq 要用 Bitmap 状态,见 §4);
- 版本化:口径变更时新建 MV,双跑对比再切流量。
2.3 ROLLUP / CUBE:小心组合爆炸
SQL 里的 CUBE / ROLLUP 可以一次算出多个维度小计,很好写,也很容易把中间结果撑爆:
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 种组合;维度再加,存储与写入线性到指数恶化。实务建议:
- 产品看板需要的小计显式建 MV,而不是对明细开任意 CUBE;
- 前端「合计行」优先在 API 层对已有聚合结果再求和(可加性指标),避免每次下推 CUBE;
- 不可加指标(UV、CTR)不能对子节点简单相加——必须回到 Bitmap/定义式重算。
3. 漏斗分析:有序递减,而不是四个独立计数
漏斗每一步都是「穿过了上一步的用户集合」再求基数。
3.1 语义要件
- 步骤有序:同一用户先曝光、再点击、再落地、再转化(或归因窗口内可连上);
- 窗口:会话漏斗、当日漏斗、7 日归因窗——产品必须选一种写进口径文档;
- 主体:通常按
user_id/device_id;注意登录态与设备图混淆; - 去重粒度:步骤内 UV 要去重,步骤间靠集合运算。
错误示范:用「点击 PV / 曝光 PV」当 CTR,又在漏斗第二步用 UV——上下游不可比,广告主一定会来对线。
3.2 工程实现素描
- 明细路径:用户事件按 key 排序/窗口,状态机走过步骤(Flink CEP 或窗口 Session);适合实时漏斗大屏。
- Bitmap 路径:每日每步骤落一张用户 Bitmap,漏斗 = 逐步 AND 后再
cardinality——适合日报与看板,且可复现。
转化与点击之间若走广告归因(点击归因 / 末次曝光),漏斗步骤边界要以归因结果表为准,而不是原始转化日志时刻——否则和结算对不上。
3.3 漏斗的常见歪楼
| 歪楼 | 后果 | 纠正 |
|---|---|---|
| 用 PV 当漏斗步骤 | CTR 虚高、同用户膨胀 | 步骤一律 UV |
| 窗口与结算归因窗不一致 | 看板 CVR ≠ 账单 | 对齐归因文档 |
| 步骤时间戳用 processing-time | 乱序下漏算 | event-time + 允许迟到 |
| 跨设备未打通却宣称「用户漏斗」 | 同人多次计 | 明确主体是设备还是人 |
| 把「可见曝光」与「竞价成功」混为一步 | 优化方向跑偏 | 拆开定义 |
漏斗是产品语义,不是 SQL 语法糖——先写进口径中心,再写查询。
4. 精确去重 vs 近似:Bitmap 与 HLL
交集问法(漏斗、留存)几乎点名 Bitmap;HLL 擅长「大概多少」。
4.1 Bitmap(Roaring)
- 用户 ID 映射成整数(字典表或哈希到 64-bit 后再二次编码);
- 按
date × 维度存 Bitmap 列; - 留存:
bitmapAnd(d0, d7);漏斗:逐步bitmapAnd; - 体积与基数/分布相关,高基数维度要控制粒度(先按天、按计划,避免按所有维笛卡尔)。
ClickHouse AggregateFunction(groupBitmap, …) / Bitmap* 函数族、Doris Bitmap 类型,都是广告 UV 的常用武器。
4.2 HLL
- 固定内存、可合并、适合超高基数趋势;
- 无法干净表达「集合交」;
- 误差对老板大屏可接受,对账单 UV、结算 DAU不可接受。
规则:钱相关、合同相关 → Bitmap/精确;体感相关 → HLL 可。 含糊就会在客诉时付出十倍成本。
4.3 字典爆炸与基数治理
Bitmap 前面的「用户 ID → 整数」字典本身可能成为瓶颈:全站设备 ID 几十亿时,全局字典不可持。常见拆法:
- 按天局部编号(仅服务当日交并);跨日留存则用稳定 hash 到 64-bit 再 Roaring(容忍极低碰撞)或分段字典;
- 按广告主分片字典:广告主后台查询天然带租户过滤;
- 冷用户淘汰:超长尾设备不进精细 Bitmap,只进 HLL 趋势。
没有基数治理的 Bitmap,会在「终于做对 UV」的第二天把磁盘写满。
5. 留存:cohort 的集合运算
N 日留存本质是:
在 D0 进入队列的用户集合,有多少出现在 D0+N 的活跃集合里。
落地:
- D0 转化用户 Bitmap(或首曝用户);
- 之后每日活跃 Bitmap(有曝光/打开/回流);
retain = |B0 ∩ BN| / |B0|。
预聚合时按 cohort_date × N × 维度 存分子分母,看板只读 ADS。不要每次从明细重放 30 天事件流——除非你在做口径研究。
6. 报表 API 加速清单
Serving 不只是「查 OLAP」:
| 手段 | 作用 | 注意 |
|---|---|---|
| 物化 / Rollup | 降扫描量 | 覆盖高频组合 |
| 分区 + 排序键 | 裁剪与局部性 | 与过滤条件对齐 |
| API 结果缓存 | 挡重复看板刷新 | 键含口径版本与时区 |
| 布隆 / 空缓存 | 挡乱 ID 穿透 | 见 布隆过滤器 |
| 查询隔离 | 看板与 dig 分队列 | 避免 explorative SQL 打死集群 |
| 限流与异步导出 | 兜底长尾大查询 | 超限改任务系统 |
缓存层要带口径版本号:发版切换 MV 时旧缓存必须失效,否则广告主看到「两套数轮播」。
6.1 接口契约建议
报表 API 对外至少固定这些字段:
metric_version/dict_version:口径与字典版本;data_as_of:数据稳定到的事件时间上界;partial:当前窗是否仍可能因迟到而上升;query_path:hit_mv / hit_cache / scan_detail(便于排查,不一定暴露给广告主)。
前端展示「数据更新至 xx:xx」比盲目 15 秒轮询有价值得多——也减少对集群的无效击打。
7. 与实时仓、Flink 的接合点
- 写入:Flink 以 exactly-once / 幂等 sink 写入 DWD,再驱动 MV 或下游 ADS;
- 迟到:watermark 与归因窗决定「报表是否还会变」——产品需展示「数据稳定时间」;
- 补数:Kappa 重放后 ADS 覆盖写,API 缓存整层失效;
- 对账:日终用同一份 Bitmap/指标定义与结算域对比,diff 进入告警而不是人工 Excel。
7.1 从竞价到报表的指标闭链
一条曝光从出价到进看板,途经:竞价日志 → 曝光上报 →(可选)可见性校准 → 计费 → DWD → ADS。闭链检查问三个问题:
- 水量:竞价成功数与曝光数比例是否在合理带?
- 金额:计费花费 ∑ 是否等于结算域?
- 人:看板 UV 是否与去重规则下的计费侧一致?
任何一环用「另一套近似」偷懒,都会在广告主对账单时被揪出来。布隆过滤器适合挡不存在的 ID击穿缓存(见 布隆),但不能充当 UV 真相。
参考
- ClickHouse Docs. Functions for Working with Bitmaps:Bitmap 与漏斗/留存实现入口。
- Apache Doris Docs. Bitmap:Doris Bitmap 类型与聚合。
- ClickHouse. Materialized Views:增量物化与投影思路。
- 本系列. 列式存储 · 实时数仓分层。
- Flink 深挖开篇. 流处理模型与运行时架构:实时写入侧的作业心智。