做营销自动化这事,我踩过最深的坑不是人群包跑不出来,而是数据底座先散架。最开始线上线下各系统各算各的:广告平台说点击二十万,埋点平台统计十八万,订单库里的关联转化又对不上,运营开周会的时候对着三份 Excel 反复拉扯。多源数据要真正驱动营销自动化,一个统一的 OLAP 层几乎是绕不开的阶段,而这背后就是一套持续演进的架构设计。
这篇文章适合正在搭营销数据平台、增长数据中台,或者想把离线数仓往准实时 OLAP 架构迁移的工程师。我会把多源数据 OLAP 架构演进里真正踩过的坑和做过的取舍写清楚,包括数据源怎么盘、引擎怎么选、分阶段怎么演进、指标怎么建模、一致性怎么处理,以及 OLAP 上线后怎么支撑人群圈选和自动触达闭环。目标就一个:你读完能直接拿去对号入座,而不是看一堆概念名词。
1. 营销自动化的数据盘面:这五类数据源是架构的起点
很多人一开始就把精力放在引擎选型上,这是本末倒置。营销自动化的 OLAP 架构要解决什么问题,完全取决于要和哪些数据源打交道。我把它盘成了五类,每一类的数据特征、接入方式和踩坑点都不一样。
1.1 广告平台投放数据:高频、强时效、字段多变
投放侧每天产生计划、广告组、创意各个层级的消耗、展示、点击、转化回传,数据主要来自各家广告平台的 API。这类数据有几个很麻烦的特点。
第一,接口配额紧。平台一天就给你那么多次调用额度,你还要拉好几个维度的报表,配额根本不够用。第二,字段口径不统一。有的平台叫“展示”,有的叫“曝光”,有的叫“展示量”,同一个词在不同平台语义可能完全不同。第三,回传有延迟。广告平台给你回传的转化数据,经常是 T+1 甚至 T+3 补垄的,你今天看到的 ROI 明天可能还会变。
这类数据的处理方式,我建议是定时同步 API,落到 OLAP 里用 Unique 模型做 upsert。因为你没法假设平台只产生增量,它经常修正历史数据——昨天说消耗一万,今天改成九千,这种场景 append 型存储根本扛不住。
1.2 埋点行为数据:流量大、延迟低、是营销漏斗的主体
网站、H5、APP 上的埋点事件,是营销自动化分析里量级最大的一部分。曝光、点击、浏览、加购、注册、下单,这些事件通过 Kafka 实时进入数据链路。营销漏斗的主体就在这里,没有它,你根本不知道用户从看到广告到最终转化之间经历了什么。
埋点数据的接入,核心是事件命名治理。我见过太多团队,event_name 里混着大小写、空格、中文,甚至同一个事件叫三个名字。这件事必须在埋点规范阶段就定死,不然后面清洗逻辑会越写越脏。另外,埋点数据天然就是明细型的,要原样保留,不要在上游就做聚合,否则后续任何分析都受限。
1.3 CRM 与订单业务库:事实钱包数据,需要 CDC
订单、客户、优惠券、会员等级这些数据,通常放在 MySQL 或者 PostgreSQL 业务库里。这是“钱”的数据,也是转化承接的最终事实。营销自动化里所有 ROI、LTV 的最终计算,都要回到这一层。
这类数据源的接入,千万别用业务库直连。我的做法是用 CDC(Change Data Capture)从 Binlog 同步,交给 Canal 或者 Debezium 解析,再进 Kafka,最后由 Flink 写入 OLAP。原因很简单:一个是不能因为分析查询拖垮线上交易库,另一个是营销系统需要准实时看到订单变化,而不是每天全量拉一次。
1.4 多源数据带来的三类基础问题
把这五类数据源放一起,你会发现三个绕不开的问题。
第一个是口径问题。点击、曝光、ROI,在广告平台、埋点系统和财务系统里定义完全不一样。广告平台的“点击”可能是去重后的,埋点的“点击”可能是带参数的,财务看的“成交”又是指已支付订单。第二个是 ID 打通问题。未登录用户用 device_id,登录用户用 uid,广告平台又用点击回调的 click_id,多套 ID 之间要做映射,否则同一个用户在事件表里是三个人。第三个是时效性割裂。报表工具直连业务库,数据只有 T+1,营销自动化想实时圈人群却拿不到数据。
这三个问题不是靠堆数据工具能解决的,必须靠一个分层合理、口径统一的 OLAP 建模层。这也是为什么我要说,架构演进之前,先盘数据。
2. 引擎选型不是越新越好:按营销分析场景倒推 OLAP 方案
很多团队一聊 OLAP 就先问“哪个引擎最快”,我的答案是:先看你的查询长什么样。营销分析场景的查询特征非常鲜明,拿这些特征去倒推引擎,选型才不会翻车。
2.1 营销分析查询的三个典型特征
第一个特征是多维明细检索。运营和投放人员要按渠道、计划、日期组合筛选,甚至要看某个人群的明细记录。这种查询是典型的 point query 加多维过滤,不是单纯的大宽表扫描。
第二个特征是高基数精确去重。计算 UV、转化用户数、留存率、LTV,都要对 uid 或者 device_id 做海量去重。这里的难点是“精确”两个字。用近似去重,运营可能会因为误差多花钱或者少花钱,所以金额相关指标必须精确。
第三个特征是漏斗和归因计算。从曝光到点击到加购到支付,要跨多个事件类型做关联,还要把广告平台的点击和后续订单关联起来。这类查询要处理大量的 join 和窗口聚合,分析窗口通常是近 7 天、近 30 天。
2.2 主流 OLAP 引擎对比:ClickHouse、Doris、StarRocks
拿这三个引擎对比,是因为它们是目前营销数据场景里被讨论最多的。
| 对比维度 | ClickHouse | Apache Doris | StarRocks |
|---|---|---|---|
| 明细写入能力 | MergeTree 高性能写入,更新能力弱 | Duplicate 模型适合明细,Unique 支持 upsert | Primary Key 模型支持较好 |
| 更新与修正 | ReplacingMergeTree / AggregatingMergeTree,需批量 | Unique Key 模型,Merge-on-Write 实时性好 | 主键表默认 Merge-on-Write |
| 多表 Join | 偏弱,依赖物化视图或大内存 | Colocate Join / Bucket Shuffle Join | Colocate Join 更成熟 |
| 高并发点查 | 一般,需复杂规划 | 较好,适合人群圈选类查询 | 较好 |
| 高基数精确去重 | uniqExact 内存开销大,可转 bitmap | BITMAP 类型 + 精确去重,配合物化视图 | BITMAP 类型支持较好 |
| 运维成本 | 单机能力强,集群需外部组件支撑 | FE/BE 两套进程,部署较重 | 与 Doris 类似,但迭代更快 |
结论很直接:营销自动化场景里,要频繁处理实时 upsert、多表关联、高基数精确去重、高并发人群圈选,Doris 和 StarRocks 会更顺手。ClickHouse 并不是不好,它更适合超大规模日志类实时分析,但营销分析并不是纯日志场景。
2.3 为什么不能只靠离线数仓硬撑
有人会问,Hive/Spark 数仓不是也能做这些吗?确实能做,但那是 T+1 的节奏。离线数仓解决的是口径和存储问题,查询延迟动辄几秒到几分钟,没法支撑营销人员在投放过程中做实时决策。
营销自动化的特点是“看完数据要马上做动作”——建人群、放量、暂停素材,这些动作都要基于分钟级的数据反馈。所以我的建议是:离线数仓和实时 OLAP 不是二选一,而是并存。离线跑全量重算和对账,实时支撑及时决策,分层模型两边复用。
3. 三阶段演进实录:从 Excel 直连到实时 OLAP 的关键转折
架构不是一天搭出来的。我复盘自己经历过的升级过程,基本可以分成三个阶段,每个阶段都有它崩溃的节点和演进的动机。
3.1 阶段一:业务库直连加报表工具,活不过三个月
这是很多团队的第一版:MySQL 前置,BI 工具直接连业务库写 SQL。初期数据量小,问题不明显。一旦报表数量上来,运营、投放、财务都开始用,问题就炸了。
频繁的复杂查询直接拖垮业务库,慢查询把线上交易的性能都带崩了。每张报表背后是一套 SQL,同一个人问我“点击是多少”,不同报表能给出三个数,运维每天被拉去开会解释数据差异。这个阶段的教训是:营销分析查询绝不能和业务在线库共享资源,物理隔离是底线。
3.2 阶段二:T+1 离线数仓,口径收敛了但时效掉队
被阶段一逼着,我们上了 Hive/Spark 离线数仓,定时任务每天凌晨清洗数据,产出统一的宽表和指标。口径确实收敛了,报表也稳定了,数据团队终于不用天天背锅。
但新的问题很快冒出来:只能看到昨天的数据。早上数据跑完,运营一看已经过时了。A/B 实验要想当天看效果,做不到;投放要实时看素材消耗,也做不到。运营对 T+1 数据越来越不信任,因为广告平台后台的实时数据和他们手上的报表就是差一截。这个阶段最大的贡献,是把口径和分层模型沉淀下来了,为实时化铺了路。
3.3 阶段三:Kafka + Flink + OLAP 的准实时管道
实时链路是我们演进的重头戏,它的整体结构是这样的:
- 埋点链路:APP/Web 埋点 → Kafka → Flink 清洗、ID 映射 → Doris/StarRocks DWD 明细表
- 广告 API 链路:定时拉取广告平台数据 → 清洗 → Kafka → Flink → DWS 聚合表 upsert
- 订单库链路:MySQL Binlog → Canal/Debezium → Kafka → Flink → DWD 订单事实表加 DWS 聚合
分层模型沿用离线数仓的体系:DWD 明细层存原始事实,DWS 聚合层按天和维度预聚合,ADS 应用层服务报表与指标系统。实时链路不是要把离线干掉,离线负责全量重算和对账,实时负责分钟级增量。
| 阶段 | 时效性 | 查询并发 | 口径一致性 | 维护成本 |
|---|---|---|---|---|
| 阶段一 | 实时直连(但拖垮库) | 极低 | 混乱 | 低 |
| 阶段二 | T+1 | 中 | 统一 | 中高 |
| 阶段三 | 分钟级 | 高 | 分层统一 | 中高(可控) |
3.4 演进的关键:先想清楚要解决什么业务问题
数据架构演进不是为了赶时髦。阶段一走到阶段二,是因为口径混乱已经影响业务信任;阶段二走到阶段三,是因为营销自动化的核心诉求从“事后看报表”变成了“实时调整投放”。
每走一步,我都会问自己三个问题:查询耗时是否明显下降?报表口径对账是否通过?运营人员自助提数是否变快了?如果这三个答案都是肯定的,说明架构演进的投入是值得的。如果只是把引擎换了,指标还是对不上,那营销团队很快会失去信任,后面再想推动任何改造都很困难。
4. 埋点、订单与广告消耗的建模:营销指标在 OLAP 里的落地方式
架构搭起来了,真正的难点在建模。同样的数据,模型设计得好不好,查询性能能差出几十倍。我挑三个核心场景,讲清楚我们是怎么在 OLAP 里落地的。
4.1 DWD 明细表:营销事件表的建表细节
先看我们埋点事件表的 DDL,以 Apache Doris 2.x / StarRocks 3.x 语法为准,不同版本细节略有差异:
CREATE TABLE dwd_traffic_event ( event_id VARCHAR(128) NOT NULL COMMENT '事件唯一ID,服务端生成', dt DATE NOT NULL COMMENT '事件日期(本地时区)', event_time DATETIME NOT NULL COMMENT '事件产生时间', uid BIGINT NULL COMMENT '登录用户映射ID', device_id VARCHAR(64) NULL COMMENT '匿名设备ID', event_type VARCHAR(32) COMMENT 'impression/click/cart/order/payment', campaign_id BIGINT COMMENT '广告计划ID', ad_group_id BIGINT COMMENT '广告组ID', creative_id BIGINT COMMENT '创意ID', channel VARCHAR(64) COMMENT '渠道标识', attributed_campaign_id BIGINT COMMENT '归因后的计划ID', page_url VARCHAR(512) COMMENT '落地页URL', extra_json JSON COMMENT '扩展字段' ) DUPLICATE KEY(event_id) PARTITION BY RANGE(dt)() DISTRIBUTED BY HASH(uid) BUCKETS 48 PROPERTIES ( "replication_num" = "3", "dynamic_partition.enable" = "true", "dynamic_partition.time_unit" = "DAY", "dynamic_partition.start" = "-60", "dynamic_partition.end" = "3" );几个设计决策,我逐个解释。
明细表用 Duplicate Key 模型,不丢任何原始事件。event_id 作为重复键,它的作用是业务幂等去重,不是数据库唯一约束。按天分区是因为营销查询一定带时间范围,分区裁剪能把扫描量减到最小。按 uid 分桶,是因为去重、join 都依赖用户维度,同一用户能落到同一个桶里。动态分区保留最近 60 天,更老的数据转冷存储或者对象存储,控制成本。
4.2 DWS 聚合表:把高频指标提前算好
明细表是万能的,但它太大,不适合高频报表直接扫。所以我们把广告消耗这类高频查询做成 DWS 聚合表:
CREATE TABLE dws_ad_cost_daily ( dt DATE NOT NULL, platform VARCHAR(32) NOT NULL COMMENT '广告平台', campaign_id BIGINT NOT NULL COMMENT '广告计划ID', cost DECIMAL(12,4) DEFAULT 0, impressions BIGINT DEFAULT 0, clicks BIGINT DEFAULT 0, conversions BIGINT DEFAULT 0, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ) UNIQUE KEY(dt, platform, campaign_id) DISTRIBUTED BY HASH(campaign_id) BUCKETS 16 PROPERTIES ("replication_num" = "3");这里用 Unique Key 模型,是因为广告平台的数据会有修正。同一个 dt、platform、campaign_id 下,消耗和点击数据会被平台调整,Unique Key 模型支持按 key 直接覆盖,保证历史修正能落到表里。
有团队会纠结用 Aggregate 模型加 SUM 聚合,但我建议用 Unique 模型,因为有些字段不是简单加法的,比如去重后的转化人数,你直接 SUM 就重复了。Unique 模型更通用。
4.3 漏斗、ROI、LTV 口径怎么固化进数据层
口径不统一是营销数据分析最大的痛点。我的经验是:口径必须在 DWD 层就打上标签,而不是在报表层临时算。
拿 ROI 归因来说,最怕的就是各系统各算各的。我们实践下来,提前确定归因窗口(7 天还是 30 天)和归因模型(首次点击、末次点击、线性归因),然后在 Flink 清洗阶段把归因后的 campaign_id 写入事件表的 attributed_campaign_id 字段,所有下游报表只认这个字段。这样做的好处是简单粗暴,报表层不用再讨论口径。
LTV 计算则必须回到订单明细,不能只留聚合值。把订单表和事件表按 uid join,算出每个用户从首次获客到当前时间产生的累计价值。高基数精确去重要用 BITMAP:Doris/StarRocks 里先把 uid 映射成 BIGINT,再用 bitmap_union 和 bitmap_count 算精确去重。如果只是要个量级参考,用 HLL 近似就够了,但涉及金额、费用的指标我一律用精确去重。
4.4 维度表变化:广告计划改名、渠道调整怎么办
营销运营经常改广告计划名称,调整渠道分组。如果事件表里只存 campaign_id,要知道名称必须关联维表,然后维表一变,历史统计口径就乱了。
我的实践建议是:营销分析场景优先宽表冗余。把 campaign 名称、渠道、负责人这些低频变化的字段,冗余到事件表或者 DWS 聚合表里,查询不用关联,口径被冻结在写入那一刻。如果确实需要追踪维表历史变化,可以考虑 SCD2,但营销分析场景里,绝大多数时候宽表冗余是性价比最高的方案。牺牲一点存储,换来查询性能和口径稳定,非常值得。
5. 一致性、迟到数据与大表 Join:OLAP 工程里最硬的四块骨头
建模建好了,不代表数据就靠谱了。OLAP 上线之后,真正考验工程师的是数据一致性、迟到数据、大表关联这些工程细节。这四块骨头,我每块都啃过。
5.1 重复数据和幂等写入:为什么事件表必须有 event_id
流式计算一旦重启,Kafka 至少一次语义加 Flink 重放,很容易造成重复写入。OLAP 引擎本身不会自动识别业务上的重复事件,特别是 Duplicate 模型,它就是把数据原样存进去。
所以我把 event_id 当作硬约束:所有事件在源头必须生成唯一 ID,下游做一切去重都依赖它。校验方法很简单,每天对一遍 DWD 表:
select dt, count(1) as total_cnt, count(distinct event_id) as unique_cnt, count(1) - count(distinct event_id) as dup_cnt from dwd_traffic_event where dt = '2025-01-15' group by dt;差值超过阈值就告警,排查是不是 Flink 任务重复写了。把幂等保障放在上游,比在 OLAP 里做低效去重要靠谱得多。
5.2 迟到数据与归因窗口:营销场景的特殊挑战
营销数据有一个特点,广告平台的转化回传会晚到好几天。用户看了广告,5 天后才下单,这条转化事件发生在 5 天后,但如果按 7 天归因窗口来算,它应该归属到 5 天前的那次点击。
这个逻辑在离线数仓里很简单,跑一次全量重算就完了。但在实时链路里就麻烦了:实时表今天写入一条转化,你用 Unique 模型把它盖到点击发生那天的分区,没问题;但如果这个转化后面又被平台修正,你要能再次覆盖它。所以我们规定了一个原则:实时链路不能设计成“只能追加、不能重算”的形态,DWS 聚合表要提供重算接口,每天早上对近 7 天的归因窗口做一次校准任务,保证历史指标是准的。
5.3 大表关联:用分桶设计和 Colocate Join 把 SQL 效率跑起来
营销分析里最重的查询,是事件表 join 订单表算转化,两个表都几十亿行,join 一不小心就把集群跑挂。
Doris/StarRocks 的解决思路是 Colocate Join:两张表都用DISTRIBUTED BY HASH(uid)分桶,分桶数保持一致,再使能 colocate 属性,这样关联时数据就在本地桶内完成,不需要 shuffle。如果两张表分桶列不一致,查询就会退化成大规模分发,基本等于告诉集群“我要跑一个全量 join”。
如果引擎不支持 Colocate Join,或者表实在没法保证分桶一致,另一个高性价比思路是宽表:把订单表的最新状态实时冗余到事件表,查询之前先把需要 join 的字段都塞进去。营销分析的维度相对稳定,宽表牺牲一点存储空间,换取查询不再 join,这是非常划算的取舍。
5.4 实时链路可观测性与对账
实时数仓比离线数仓更需要监控。离线的坏了第二天看日志能查到,实时链路坏了 10 分钟,营销自动化系统可能已经发出几万条错误触达。
所以我建议至少要监控这几项:Kafka topic 的消费延迟、Flink checkpoint 失败率、OLAP 导入失败的条数、导入时延、还有 DWD 表的重复率。另外,每天固定时间跑一个对账任务,用离线数仓和实时表对比昨天的曝光、点击、订单金额,差值超过阈值就告警。这个对账机制非常重要,没有它,实时数仓上线三个月后,没人敢信里面的数据。
6. 从报表到决策:OLAP 支撑营销自动化的三个闭环场景
OLAP 架构的最终价值,不是让报表变快,而是让数据真正驱动营销决策和自动化动作。我讲三个我们已经跑通的闭环场景。
6.1 人群圈选:从“数仓取数”变成“服务化查询”
以前运营要圈一个“近 30 天点击过 A 活动但未下单”的人群,流程是提需求给数仓,数仓写 SQL,跑出来导出文件,再交给触达系统,一等就是几小时甚至一天。
上了 OLAP 之后,人群圈选就是一个带过滤条件的明细查询。把渠道、时间范围、事件类型、行为条件拼成 SQL,对 DWD 明细表做过滤,返回 uid 集合,通过 BITMAP 或者直接导出进入触达系统。几十亿行明细,秒级返回,这是 OLAP 引擎的高并发点查能力带来的质变。
这里我建议做一层 SQL 模板服务,不对外暴露原始表,由应用层拼接参数,防止运营同学一时手滑写了个全表扫描把集群拖垮。
6.2 自动化触达与效果回流
营销自动化系统定时从 OLAP 取人群和指标,执行推送、短信、邮件触达,触达结果再回流写入 DWD,和后续的转化事件关联。这就是一个完整的数据动作闭环。
数据驱动开始变成“系统根据 OLAP 里的实时指标自动决定触达策略”,而不是“人看报表再决定”。自动化对数据稳定性的要求急剧上升,因为一旦数据出错,影响是自动放大的。所以第 5 章里讲的那些监控和对账,在这个阶段不是可选项,而是必选项。
6.3 A/B 测试与素材优化
A/B 测试过去很痛苦,因为实验数据要等 T+1,看到结果时投放预算都花完了。有了 OLAP 明细层之后,实验组和对照组可以直接对 DWD 表做 SQL 聚合,实时看转化率、成本、显著性。
这里有一个小建议:实验分桶键尽量用 uid,因为你后面大概率要 join 订单和事件表算 LTV,用 uid 分桶能利用上 Colocate Join 的能力。素材级别的曝光、点击、成本对比就更简单了,在 DWS 聚合表里按 creative_id 出报表,投放人员自己就能看,不再需要每次找数据团队。
6.4 架构演进带来的团队与流程变化
OLAP 架构落地之后,最大的变化是口径收敛。运营不再拿广告平台后台和公司报表对喷,因为底层数据已经统一到一个模型里。数据团队从天天写临时 SQL 接需求,变成专心维护口径、核对数据质量、优化查询性能。
但要清醒一点:OLAP 只是存储和查询底座,真正让数据可信的是口径定义、数据质量校验和权限治理。未来团队可以考虑在 OLAP 之上加一层语义层或者指标平台,让非技术团队不用写 SQL 也能配置指标、自助分析。OLAP 不会消失,它会下沉成整个营销数据体系的基座。
如果让我重来一次,我可能不会在实时链路上一上来就那么激进。踩过几次坑之后,我觉得架构演进的节奏比技术选型更关键:先让业务方看到 T+1 口径被统一的好处,再上分钟级实时;先打通一个核心场景,再铺开群体。每走一步都能被业务验证,数据团队才有下一次演进的空间。OLAP 不是终点,但它是让营销自动化从口号变成真闭环的那块地基。