多渠道广告归因数仓实践

整合 Google、微信、抖音、微博等多渠道的曝光、点击、转化事件,构建统一的用户触点旅程,输出 Last Touch、Linear、Position-Based 三种归因模型对比报告及广告 ROI 分析。本文以 5 个用户、20 条多渠道广告事件为主数据集,端到端演示 OSS PIPE + Kafka PIPE → ODS → DWD → DWS → ADS 的完整构建过程,并额外使用 3 条临时事件验证 Table Stream 捕获效果,覆盖 Dynamic Table 归因增量计算、Inverted Index 活动名称搜索、Table Stream 转化事件捕获三项关键平台能力的落地用法。


概述

多渠道广告归因的核心挑战是:不同渠道的事件数据格式各异、用户 ID 体系不统一,同一次转化可能被多个渠道各自"认领"。

云器 Lakehouse 通过以下组合解决这几个核心问题:

问题解决方案
多渠道原始事件结构各异,需统一摄取OSS PIPE(GA 文件导入)+ Kafka PIPE(实时点击流),统一写入 Bronze 层
ODS → DWD → DWS → ADS 自动增量计算Dynamic Table,CTE 化归因模型,系统自动按依赖链调度刷新
同一转化在三种归因模型下需并行计算三张独立 DWS Dynamic Table,并行持有 Last Touch / Linear / Position-Based 结果
活动名称关键词搜索(如查所有"video"活动)Inverted Index,秒级全文检索 campaign_name
新增转化事件需触发归因重算Table Stream,捕获 bronze_ad_events 的新增记录,驱动下游刷新

涉及的 SQL 命令

命令 / 函数用途说明
CREATE TABLE
CREATE TABLE
建 ODS 层事件表和活动元数据表普通表,作为 Dynamic Table 上游
CREATE BLOOMFILTER INDEX
CREATE BLOOMFILTER INDEX
user_id
user_id
列创建 Bloomfilter 索引
高基数列点查,DWD 层 JOIN 时加速过滤
CREATE INVERTED INDEX
CREATE INVERTED INDEX
campaign_name
campaign_name
列创建倒排索引
支持
MATCH_ALL
MATCH_ALL
全文搜索活动名称关键词
CREATE PIPE
CREATE PIPE
创建 OSS / Kafka 持续摄取管道分别对应 GA 文件自动导入和实时点击流摄取
CREATE TABLE STREAM
CREATE TABLE STREAM
捕获 bronze_ad_events 新增记录APPEND_ONLY 模式,驱动归因增量刷新
CREATE DYNAMIC TABLE
CREATE DYNAMIC TABLE
创建 DWD / DWS / ADS 层增量计算表三层自动按依赖链增量刷新
REFRESH DYNAMIC TABLE
REFRESH DYNAMIC TABLE
手动触发一次刷新初次构建或调试时使用

前置准备

本文所有示例在

best_practice_ad_attribution
best_practice_ad_attribution
Schema 下运行。

CREATE SCHEMA IF NOT EXISTS best_practice_ad_attribution;


ODS 层:多渠道原始事件表

建表

CREATE TABLE IF NOT EXISTS best_practice_ad_attribution.bronze_ad_events ( event_id STRING, user_id STRING, channel STRING, event_type STRING, -- impression / click / conversion event_time TIMESTAMP, campaign_id STRING, creative_id STRING, platform STRING, -- web / app region STRING, ingest_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP() -- PIPE 写入时自动填充 );

CREATE TABLE IF NOT EXISTS best_practice_ad_attribution.bronze_campaign_meta ( campaign_id STRING, campaign_name STRING, channel STRING, budget DOUBLE, start_date DATE, end_date DATE, creative_id STRING, creative_type STRING -- text / image / video );

创建 Bloomfilter Index

DWD 层归因 SQL 会按

user_id
user_id
做大量 JOIN 过滤,
user_id
user_id
属于高基数列,适合 Bloomfilter Index。

CREATE BLOOMFILTER INDEX IF NOT EXISTS idx_bf_user_id ON TABLE bronze_ad_events (user_id);

创建 Inverted Index

运营团队需要按关键词搜索广告活动,例如查所有包含"video"的活动。

PROPERTIES('analyzer'='english')
PROPERTIES('analyzer'='english')
启用英文分词。

先写入活动元数据,供 DWD 层补全活动名称、创意类型和预算,也供 Inverted Index 搜索使用:

INSERT INTO best_practice_ad_attribution.bronze_campaign_meta (campaign_id, campaign_name, channel, budget, start_date, end_date, creative_id, creative_type) VALUES ('camp_001','Google Search Spring Promo', 'google', 50000, CAST('2024-01-01' AS DATE), CAST('2024-01-31' AS DATE), 'cr_g1', 'text'), ('camp_001','Google Search Spring Promo', 'google', 50000, CAST('2024-01-01' AS DATE), CAST('2024-01-31' AS DATE), 'cr_g2', 'text'), ('camp_002','WeChat Moments Brand Awareness', 'wechat', 80000, CAST('2024-01-01' AS DATE), CAST('2024-01-31' AS DATE), 'cr_w1', 'video'), ('camp_002','WeChat Moments Brand Awareness', 'wechat', 80000, CAST('2024-01-01' AS DATE), CAST('2024-01-31' AS DATE), 'cr_w2', 'image'), ('camp_003','Douyin Short Video Retargeting', 'douyin', 120000, CAST('2024-01-01' AS DATE), CAST('2024-01-31' AS DATE), 'cr_d1', 'video'), ('camp_003','Douyin Short Video Retargeting', 'douyin', 120000, CAST('2024-01-01' AS DATE), CAST('2024-01-31' AS DATE), 'cr_d2', 'video'), ('camp_004','Weibo Topic Engagement', 'weibo', 30000, CAST('2024-01-01' AS DATE), CAST('2024-01-31' AS DATE), 'cr_wb1', 'image');

CREATE INVERTED INDEX IF NOT EXISTS idx_inv_campaign_name ON TABLE bronze_campaign_meta (campaign_name) PROPERTIES('analyzer'='english');

构建索引(使存量数据可被搜索):

BUILD INDEX idx_inv_campaign_name ON bronze_campaign_meta;

验证全文搜索效果:

SELECT campaign_id, campaign_name, channel FROM best_practice_ad_attribution.bronze_campaign_meta WHERE MATCH_ALL(campaign_name, 'video');

campaign_id campaign_name channel ----------- ---------------------------------- ------- camp_003 Douyin Short Video Retargeting douyin camp_003 Douyin Short Video Retargeting douyin

两行结果对应同一活动下的两个素材(

cr_d1
cr_d1
cr_d2
cr_d2
),均命中"video"关键词。

配置 OSS PIPE(GA 文件导入)

Google Analytics 每日导出的日志文件落在 OSS 存储桶,通过 OSS PIPE 的 LIST_PURGE 模式自动扫描新文件并写入 Bronze 层:

-- 先创建 OSS 存储连接(替换为实际 AK/SK 和 endpoint) CREATE STORAGE CONNECTION IF NOT EXISTS conn_oss_ga TYPE = 'OSS' ACCESS_ID = '<your-access-id>' ACCESS_KEY = '<your-access-key>' ENDPOINT = 'oss-cn-hangzhou.aliyuncs.com'; -- 挂载存储桶到 Volume CREATE EXTERNAL VOLUME IF NOT EXISTS vol_ga_exports TYPE = 'OSS' BUCKET = '<your-bucket>' PATH = 'ga-exports/' CONNECTION = conn_oss_ga; -- 创建 OSS PIPE,LIST_PURGE 模式扫描新增 CSV 文件 CREATE PIPE IF NOT EXISTS best_practice_ad_attribution.pipe_ga_events VIRTUAL_CLUSTER = 'DEFAULT' INGEST_MODE = 'LIST_PURGE' AS COPY INTO best_practice_ad_attribution.bronze_ad_events FROM ( SELECT $1 AS event_id, $2 AS user_id, 'google' AS channel, $3 AS event_type, CAST($4 AS TIMESTAMP) AS event_time, $5 AS campaign_id, $6 AS creative_id, $7 AS platform, $8 AS region, CURRENT_TIMESTAMP() AS ingest_time FROM @vol_ga_exports ) USING csv OPTIONS('header'='true', 'sep'=',');

配置 Kafka PIPE(实时点击流)

实时 Web / App 点击流通过 Kafka 接入,写入同一张 Bronze 表:

-- 创建 Kafka 存储连接 CREATE STORAGE CONNECTION IF NOT EXISTS conn_kafka_clickstream TYPE = 'KAFKA' KAFKA_BROKERS = '<kafka-broker>:9092'; -- 创建 Kafka PIPE,每 60 秒批量消费一次 CREATE PIPE IF NOT EXISTS best_practice_ad_attribution.pipe_kafka_clickstream VIRTUAL_CLUSTER = 'DEFAULT' BATCH_INTERVAL_IN_SECONDS = '60' AS COPY INTO best_practice_ad_attribution.bronze_ad_events FROM ( SELECT get_json_object(value, '$.event_id') AS event_id, get_json_object(value, '$.user_id') AS user_id, get_json_object(value, '$.channel') AS channel, get_json_object(value, '$.event_type') AS event_type, CAST(get_json_object(value, '$.event_time') AS TIMESTAMP) AS event_time, get_json_object(value, '$.campaign_id') AS campaign_id, get_json_object(value, '$.creative_id') AS creative_id, get_json_object(value, '$.platform') AS platform, get_json_object(value, '$.region') AS region, CURRENT_TIMESTAMP() AS ingest_time FROM READ_KAFKA( '<kafka-broker>:9092', 'ad_clickstream', -- topic 名称 '', 'cz_ad_consumer', '','','','', 'json', 'json', 0, map() ) );

写入模拟数据

从本地 CSV 导入(推荐)

-- 第一步:通过 SQL PUT 将本地 CSV 文件上传到 User Volume PUT '/path/to/your/bronze_ad_events.csv' TO USER VOLUME FILE 'bronze_ad_events.csv';

-- 第二步:从 User Volume COPY INTO 表 COPY INTO best_practice_ad_attribution.bronze_ad_events FROM USER VOLUME USING csv OPTIONS('header'='true', 'sep'=',', 'nullValue'='') FILES ('bronze_ad_events.csv');

也可直接内联插入小批量测试数据(不需要 CSV 文件):

INSERT INTO best_practice_ad_attribution.bronze_ad_events (event_id, user_id, channel, event_type, event_time, campaign_id, creative_id, platform, region) VALUES ('e001','u001','google', 'impression', CAST('2024-01-01 08:00:00' AS TIMESTAMP),'camp_001','cr_g1','web','cn-north'), ('e002','u001','google', 'click', CAST('2024-01-01 08:05:00' AS TIMESTAMP),'camp_001','cr_g1','web','cn-north'), ('e003','u001','wechat', 'impression', CAST('2024-01-01 09:00:00' AS TIMESTAMP),'camp_002','cr_w1','app','cn-north'), ('e004','u001','wechat', 'click', CAST('2024-01-01 09:10:00' AS TIMESTAMP),'camp_002','cr_w1','app','cn-north'), ('e005','u001','wechat', 'conversion', CAST('2024-01-01 10:30:00' AS TIMESTAMP),'camp_002','cr_w1','app','cn-north'), ('e006','u002','douyin', 'impression', CAST('2024-01-01 10:00:00' AS TIMESTAMP),'camp_003','cr_d1','app','cn-south'), ('e007','u002','douyin', 'click', CAST('2024-01-01 10:15:00' AS TIMESTAMP),'camp_003','cr_d1','app','cn-south'), ('e008','u002','google', 'click', CAST('2024-01-01 11:00:00' AS TIMESTAMP),'camp_001','cr_g2','web','cn-south'), ('e009','u002','google', 'conversion', CAST('2024-01-01 11:45:00' AS TIMESTAMP),'camp_001','cr_g2','web','cn-south'), ('e010','u003','weibo', 'impression', CAST('2024-01-01 12:00:00' AS TIMESTAMP),'camp_004','cr_wb1','web','cn-east'), ('e011','u003','weibo', 'click', CAST('2024-01-01 12:20:00' AS TIMESTAMP),'camp_004','cr_wb1','web','cn-east'), ('e012','u003','douyin', 'impression', CAST('2024-01-01 13:00:00' AS TIMESTAMP),'camp_003','cr_d2','app','cn-east'), ('e013','u003','douyin', 'click', CAST('2024-01-01 13:10:00' AS TIMESTAMP),'camp_003','cr_d2','app','cn-east'), ('e014','u003','douyin', 'conversion', CAST('2024-01-01 14:00:00' AS TIMESTAMP),'camp_003','cr_d2','app','cn-east'), ('e015','u004','google', 'impression', CAST('2024-01-01 14:00:00' AS TIMESTAMP),'camp_001','cr_g1','web','cn-north'), ('e016','u004','google', 'click', CAST('2024-01-01 14:05:00' AS TIMESTAMP),'camp_001','cr_g1','web','cn-north'), ('e017','u004','wechat', 'impression', CAST('2024-01-01 15:00:00' AS TIMESTAMP),'camp_002','cr_w2','app','cn-north'), ('e018','u004','wechat', 'conversion', CAST('2024-01-01 16:00:00' AS TIMESTAMP),'camp_002','cr_w2','app','cn-north'), ('e019','u005','douyin', 'click', CAST('2024-01-01 16:00:00' AS TIMESTAMP),'camp_003','cr_d1','app','cn-west'), ('e020','u005','douyin', 'conversion', CAST('2024-01-01 17:00:00' AS TIMESTAMP),'camp_003','cr_d1','app','cn-west');

开启变更跟踪并创建 Table Stream

Table Stream 用于捕获

bronze_ad_events
bronze_ad_events
的新增转化事件,触发下游 Dynamic Table 的归因重算。使用前需开启表的
change_tracking
change_tracking

ALTER TABLE best_practice_ad_attribution.bronze_ad_events SET TBLPROPERTIES ('change_tracking' = 'true'); CREATE TABLE STREAM IF NOT EXISTS best_practice_ad_attribution.stream_conversion_events ON TABLE best_practice_ad_attribution.bronze_ad_events WITH PROPERTIES ('TABLE_STREAM_MODE' = 'APPEND_ONLY');

验证 Stream 捕获效果。下面 3 条

u006
u006
事件只用于确认 Stream 能捕获创建后新增的 INSERT,不参与后续 DWD / DWS / ADS 归因统计:

INSERT INTO best_practice_ad_attribution.bronze_ad_events (event_id, user_id, channel, event_type, event_time, campaign_id, creative_id, platform, region) VALUES ('e021','u006','google','impression', CAST('2024-01-02 09:00:00' AS TIMESTAMP),'camp_001','cr_g1','web','cn-north'), ('e022','u006','google','click', CAST('2024-01-02 09:05:00' AS TIMESTAMP),'camp_001','cr_g1','web','cn-north'), ('e023','u006','google','conversion', CAST('2024-01-02 09:30:00' AS TIMESTAMP),'camp_001','cr_g1','web','cn-north'); SELECT __change_type, event_id, user_id, channel, event_type FROM best_practice_ad_attribution.stream_conversion_events;

__change_type event_id user_id channel event_type ------------- -------- ------- ------- ---------- INSERT e021 u006 google impression INSERT e022 u006 google click INSERT e023 u006 google conversion

Stream 捕获了 3 条新增记录。为了保持后续归因章节的主样本仍为 5 个用户、20 条事件,查询 Stream 后清理这 3 条临时验证数据:

DELETE FROM best_practice_ad_attribution.bronze_ad_events WHERE event_id IN ('e021', 'e022', 'e023');

下游 Dynamic Table 在下次刷新周期时,会自动感知上游新数据并增量计算。本文后续归因结果基于清理后的主数据集,Last Touch 的 google 转化数保持为 1。


DWD 层:用户触点旅程表

建表

DWD 层将 Bronze 事件表与活动元数据 JOIN,补全

campaign_name
campaign_name
creative_type
creative_type
,并通过
ROW_NUMBER
ROW_NUMBER
为每个用户的触点序列打上
touch_seq
touch_seq
编号。

CREATE DYNAMIC TABLE IF NOT EXISTS best_practice_ad_attribution.dwd_user_journey REFRESH INTERVAL 5 MINUTE VCLUSTER DEFAULT AS SELECT e.user_id, e.channel, e.event_type, e.event_time, e.campaign_id, e.creative_id, e.platform, e.region, m.campaign_name, m.creative_type, m.budget, ROW_NUMBER() OVER (PARTITION BY e.user_id ORDER BY e.event_time) AS touch_seq FROM best_practice_ad_attribution.bronze_ad_events e LEFT JOIN best_practice_ad_attribution.bronze_campaign_meta m ON e.campaign_id = m.campaign_id AND e.creative_id = m.creative_id;

手动触发首次刷新:

REFRESH DYNAMIC TABLE best_practice_ad_attribution.dwd_user_journey;

查询结果(前 10 行):

SELECT user_id, channel, event_type, event_time, campaign_name, touch_seq FROM best_practice_ad_attribution.dwd_user_journey ORDER BY user_id, touch_seq LIMIT 10;

user_id channel event_type event_time campaign_name touch_seq ------- ------- ---------- ------------------- -------------------------------- --------- u001 google impression 2024-01-01T08:00:00 Google Search Spring Promo 1 u001 google click 2024-01-01T08:05:00 Google Search Spring Promo 2 u001 wechat impression 2024-01-01T09:00:00 WeChat Moments Brand Awareness 3 u001 wechat click 2024-01-01T09:10:00 WeChat Moments Brand Awareness 4 u001 wechat conversion 2024-01-01T10:30:00 WeChat Moments Brand Awareness 5 u002 douyin impression 2024-01-01T10:00:00 Douyin Short Video Retargeting 1 u002 douyin click 2024-01-01T10:15:00 Douyin Short Video Retargeting 2 u002 google click 2024-01-01T11:00:00 Google Search Spring Promo 3 u002 google conversion 2024-01-01T11:45:00 Google Search Spring Promo 4 u003 weibo impression 2024-01-01T12:00:00 Weibo Topic Engagement 1

touch_seq
touch_seq
按时间顺序编号。u001 在 wechat 发生转化前,先经历了 google 曝光和点击(
touch_seq=1,2
touch_seq=1,2
),再经历微信曝光和点击(
touch_seq=3,4
touch_seq=3,4
),最终在微信转化(
touch_seq=5
touch_seq=5
)。


DWS 层:三种归因模型

归因模型在 DWS 层通过三张独立的 Dynamic Table 并行维护,互不干扰,方便运营团队按需对比。

Last Touch 归因

将 100% 转化价值归因给转化前最近一次触点(点击或曝光)所在渠道。

CREATE DYNAMIC TABLE IF NOT EXISTS best_practice_ad_attribution.dws_attribution_last_touch REFRESH INTERVAL 5 MINUTE VCLUSTER DEFAULT AS WITH conversions AS ( SELECT user_id, event_time AS conv_time, campaign_id, channel FROM best_practice_ad_attribution.dwd_user_journey WHERE event_type = 'conversion' ), last_touch AS ( SELECT c.user_id, c.conv_time, j.channel AS attributed_channel, j.campaign_name AS attributed_campaign_name FROM conversions c JOIN best_practice_ad_attribution.dwd_user_journey j ON c.user_id = j.user_id AND j.event_type IN ('click', 'impression') AND j.event_time <= c.conv_time WHERE j.touch_seq = ( SELECT MAX(touch_seq) FROM best_practice_ad_attribution.dwd_user_journey j2 WHERE j2.user_id = c.user_id AND j2.event_type IN ('click', 'impression') AND j2.event_time <= c.conv_time ) ) SELECT attributed_channel, attributed_campaign_name, COUNT(*) AS conversions, 1.0 * COUNT(*) AS attributed_value FROM last_touch GROUP BY attributed_channel, attributed_campaign_name;

REFRESH DYNAMIC TABLE best_practice_ad_attribution.dws_attribution_last_touch; SELECT * FROM best_practice_ad_attribution.dws_attribution_last_touch ORDER BY conversions DESC, attributed_channel DESC;

attributed_channel attributed_campaign_name conversions attributed_value ------------------ -------------------------------- ----------- ---------------- wechat WeChat Moments Brand Awareness 2 2.0 douyin Douyin Short Video Retargeting 2 2.0 google Google Search Spring Promo 1 1.0

Last Touch 模型将转化完全归因给最后触点,微信和抖音各获得 2 次转化。但这低估了 google 在用户决策路径中的前置作用(u001 先看到 google 广告才最终在微信转化)。

Linear 归因

将转化价值平均分配给路径上的每个触点。

CREATE DYNAMIC TABLE IF NOT EXISTS best_practice_ad_attribution.dws_attribution_linear REFRESH INTERVAL 5 MINUTE VCLUSTER DEFAULT AS WITH conversions AS ( SELECT user_id, event_time AS conv_time FROM best_practice_ad_attribution.dwd_user_journey WHERE event_type = 'conversion' ), touchpoints AS ( SELECT c.user_id, j.channel, j.campaign_name, j.event_time, COUNT(*) OVER (PARTITION BY c.user_id) AS total_touches FROM conversions c JOIN best_practice_ad_attribution.dwd_user_journey j ON c.user_id = j.user_id AND j.event_type IN ('click', 'impression') AND j.event_time <= c.conv_time ) SELECT channel AS attributed_channel, campaign_name AS attributed_campaign_name, COUNT(*) AS touch_count, ROUND(SUM(1.0 / total_touches), 4) AS attributed_value FROM touchpoints GROUP BY channel, campaign_name ORDER BY attributed_value DESC;

REFRESH DYNAMIC TABLE best_practice_ad_attribution.dws_attribution_linear; SELECT * FROM best_practice_ad_attribution.dws_attribution_linear ORDER BY attributed_value DESC;

attributed_channel attributed_campaign_name touch_count attributed_value ------------------ -------------------------------- ----------- ---------------- douyin Douyin Short Video Retargeting 5 2.1667 google Google Search Spring Promo 5 1.5000 wechat WeChat Moments Brand Awareness 3 0.8333 weibo Weibo Topic Engagement 2 0.5000

Linear 模型充分体现了全渠道贡献:抖音触点最多(5 次),获得最高归因价值;google 触点同样是 5 次,但用户路径较长,平均分配后价值为 1.5。微博虽然没有直接转化用户,但参与了用户的决策路径,也获得了 0.5 的归因价值。

Position-Based 归因(U 型归因)

首触点和末触点各获 40% 权重,中间触点平分剩余 20%。这一模型强调"拉新(首触)"和"促成转化(末触)"的双重价值。

CREATE DYNAMIC TABLE IF NOT EXISTS best_practice_ad_attribution.dws_attribution_position_based REFRESH INTERVAL 5 MINUTE VCLUSTER DEFAULT AS WITH conversions AS ( SELECT user_id, event_time AS conv_time FROM best_practice_ad_attribution.dwd_user_journey WHERE event_type = 'conversion' ), touchpoints AS ( SELECT c.user_id, j.channel, j.campaign_name, j.touch_seq, j.event_time, MIN(j.touch_seq) OVER (PARTITION BY c.user_id) AS first_touch, MAX(j.touch_seq) OVER (PARTITION BY c.user_id) AS last_touch_seq, COUNT(*) OVER (PARTITION BY c.user_id) AS total_touches FROM conversions c JOIN best_practice_ad_attribution.dwd_user_journey j ON c.user_id = j.user_id AND j.event_type IN ('click', 'impression') AND j.event_time <= c.conv_time ), with_weight AS ( SELECT user_id, channel, campaign_name, CASE WHEN touch_seq = first_touch AND touch_seq = last_touch_seq THEN 1.0 WHEN touch_seq = first_touch OR touch_seq = last_touch_seq THEN 0.4 ELSE 0.2 / GREATEST(total_touches - 2, 1) END AS weight FROM touchpoints ) SELECT channel AS attributed_channel, campaign_name AS attributed_campaign_name, COUNT(*) AS touch_count, ROUND(SUM(weight), 4) AS attributed_value FROM with_weight GROUP BY channel, campaign_name ORDER BY attributed_value DESC;

REFRESH DYNAMIC TABLE best_practice_ad_attribution.dws_attribution_position_based; SELECT * FROM best_practice_ad_attribution.dws_attribution_position_based ORDER BY attributed_value DESC;

attributed_channel attributed_campaign_name touch_count attributed_value ------------------ -------------------------------- ----------- ---------------- douyin Douyin Short Video Retargeting 5 2.1000 google Google Search Spring Promo 5 1.5000 wechat WeChat Moments Brand Awareness 3 0.9000 weibo Weibo Topic Engagement 2 0.5000

与 Linear 相比,Position-Based 给微信更高的权重(0.9 vs 0.83),因为微信是多次转化的末触点(40% 权重),而非平均分摊。这一模型适合"重拉新 + 重收口"的营销策略下的效果评估。


ADS 层:广告活动 ROI 报告

ADS 层汇总三种归因模型的输出,结合活动预算,输出完整的 ROI 指标供 BI 工具接入。

CREATE DYNAMIC TABLE IF NOT EXISTS best_practice_ad_attribution.ads_campaign_roi REFRESH INTERVAL 10 MINUTE VCLUSTER DEFAULT AS WITH evt_stats AS ( SELECT campaign_id, campaign_name, channel, creative_type, budget, COUNT(CASE WHEN event_type = 'impression' THEN 1 END) AS impressions, COUNT(CASE WHEN event_type = 'click' THEN 1 END) AS clicks, COUNT(CASE WHEN event_type = 'conversion' THEN 1 END) AS conversions, COUNT(DISTINCT user_id) AS unique_users FROM best_practice_ad_attribution.dwd_user_journey GROUP BY campaign_id, campaign_name, channel, creative_type, budget ) SELECT campaign_id, campaign_name, channel, creative_type, budget, impressions, clicks, conversions, unique_users, ROUND(CASE WHEN impressions > 0 THEN 100.0 * clicks / impressions ELSE 0 END, 2) AS ctr_pct, ROUND(CASE WHEN clicks > 0 THEN 100.0 * conversions / clicks ELSE 0 END, 2) AS cvr_pct, ROUND(CASE WHEN conversions > 0 THEN budget / conversions ELSE NULL END, 2) AS cost_per_conversion FROM evt_stats ORDER BY conversions DESC;

REFRESH DYNAMIC TABLE best_practice_ad_attribution.ads_campaign_roi; SELECT campaign_name, channel, impressions, clicks, conversions, ctr_pct, cvr_pct, cost_per_conversion FROM best_practice_ad_attribution.ads_campaign_roi ORDER BY conversions DESC, channel, clicks DESC;

campaign_name channel impressions clicks conversions ctr_pct cvr_pct cost_per_conversion ---------------------------------- ------- ----------- ------ ----------- ------- ------- ------------------- Douyin Short Video Retargeting douyin 2 3 2 150.00 66.67 60000 Google Search Spring Promo google 2 3 1 150.00 33.33 50000 WeChat Moments Brand Awareness wechat 1 1 1 100.00 100.00 80000 WeChat Moments Brand Awareness wechat 1 0 1 0.00 0.00 80000 Weibo Topic Engagement weibo 1 1 0 100.00 0.00 null

抖音

cost_per_conversion = 60000
cost_per_conversion = 60000
(预算 120,000 / 2 次转化),google 为 50,000,两者的每次转化成本接近,但抖音转化次数多一倍,整体 ROI 更优。微博未带来直接转化,在 Last Touch 模型下会被认为无贡献,但 Linear / Position-Based 模型中它仍有路径价值,说明多模型对比对评估辅助渠道至关重要。


注意事项

  • Bloomfilter Index 的 Schema 上下文
    CREATE BLOOMFILTER INDEX
    CREATE BLOOMFILTER INDEX
    BUILD INDEX
    BUILD INDEX
    都要求与目标表处于同一 Schema 上下文。执行前先
    USE SCHEMA best_practice_ad_attribution
    USE SCHEMA best_practice_ad_attribution
    或通过
    -s best_practice_ad_attribution
    -s best_practice_ad_attribution
    参数切换。
  • Inverted Index 存量数据
    CREATE INVERTED INDEX
    CREATE INVERTED INDEX
    只对索引创建之后新写入的数据生效,已有存量数据需执行
    BUILD INDEX
    BUILD INDEX
    才可被
    MATCH_ALL
    MATCH_ALL
    搜索到。
  • Table Stream 的历史数据:Stream 只捕获创建之后的写入,不包含开启前已存在的历史记录。如需对历史数据做全量归因,直接查询
    bronze_ad_events
    bronze_ad_events
    而非 Stream。
  • Dynamic Table 刷新顺序:DWD 层
    dwd_user_journey
    dwd_user_journey
    刷新完成后,三张 DWS Dynamic Table 才会消费新数据,ADS 层再依赖 DWS 的结果。系统根据引用关系自动确定刷新顺序,无需手动编排。
  • 归因窗口:本文的三种归因模型均统计转化前该用户的全部触点,未设置回溯天数限制。实际业务中建议增加时间窗口过滤(如"转化前 30 天内的触点"),在
    JOIN
    JOIN
    条件中加
    AND j.event_time >= c.conv_time - INTERVAL 30 DAY
    AND j.event_time >= c.conv_time - INTERVAL 30 DAY
  • cost_per_conversion 为 NULL:当活动在统计窗口内无转化时,
    cost_per_conversion
    cost_per_conversion
    返回 NULL,属于正常设计;BI 工具可将其显示为"—"。

相关文档

联系我们
预约咨询
微信咨询
电话咨询
邮件咨询