订阅更新

OneID与用户ID打通

数仓建模 20 讲 · 第 9 讲 约 13 分钟读完

本文目录
  1. 为什么需要OneID?
  2. 三种ID打通方案
  3. OneID落地的关键设计
  4. 必须注意的三个坑
  5. 面试怎么答?
  6. 总结

做出海业务之后,我才真正理解OneID有多重要。

image.png

用户从TikTok广告看到我们的短剧App → 点击下载 → 安装注册 → 看剧 → 首次充值。这个链路经历了三次"身份切换":广告平台给了一个click_id,安装的时候拿到了device_id(IDFA/GAID),注册之后才有user_id。 image.png

如果不做ID打通,这三段数据在数仓里就是三个不相关的"人"。广告点击了但"没有"安装(因为click_id和device_id对不上),安装了但"没有"注册(因为device_id和user_id对不上)。ROI根本算不清,投放团队天天追着问"我的广告效果到底怎么样?"

这就是OneID要解决的问题:把同一个自然人在不同端、不同渠道、不同身份体系下的ID,映射到一个统一的用户ID上。 image.png

这篇文章我从OneID的业务背景讲起,到三种打通方案的对比,到落地的关键设计和踩过的坑,帮你在面试中完整地讲清楚这个话题。


为什么需要OneID?

先看看一个真实用户在系统里可能有多少种身份:

ID类型 来源 生命周期
设备ID(IDFA/GAID/OAID) 手机操作系统分配 重置手机后会变
user_id App内注册后生成 长期稳定
手机号 用户绑定 可能换号
微信OpenID 微信登录授权 绑定微信账号
广告click_id 广告平台生成 单次点击有效
Cookie ID 浏览器生成 清除后失效
email 用户填写 可能有多个
image.png

同一个张三,可能在系统里有5-7种不同的ID。如果不打通:

  • DAU虚高:一个人用手机号和微信各登录一次,算成了两个日活用户
  • 留存率失真:用户昨天用设备A、今天换了设备B,按device_id算会被当成两个用户,昨天用户"流失"了、今天"新增"了一个
  • 广告归因断链:广告平台给的是click_id,App里只有user_id,两个ID对不上就没法归因
  • 用户画像不完整:未登录行为和登录后行为割裂,画像数据残缺

三种ID打通方案

方案一:确定性匹配(生产主力)

通过强关联事件直接建立ID映射关系。什么叫强关联?就是两个ID同时出现的时刻。 image.png

典型的强关联事件:

  • 用户登录时:上报了device_id和user_id → 确定这个设备属于这个用户
  • 绑定手机号时:user_id和手机号同时出现 → 确定是同一人
  • 广告归因回传时:device_id和click_id通过归因SDK关联 → 确定是同一次行为
-- 从登录日志中提取ID关系对
-- 核心逻辑:找到两个ID同时出现的事件
CREATE TABLE tmp_id_pairs AS
SELECT DISTINCT
    user_id,
    device_id,
    'login' AS relation_source,          -- 关系来源
    MIN(event_time) AS first_bindding_time -- 首次关联时间
FROM dwd_user_login_detail_di
WHERE user_id IS NOT NULL
  AND device_id IS NOT NULL
  AND dt >= DATE_SUB(CURRENT_DATE, 90)    -- 只取最近90天的关系
GROUP BY user_id, device_id;

-- 同理提取 user_id ↔ phone 的关系
INSERT INTO tmp_id_pairs
SELECT DISTINCT
    user_id,
    phone AS device_id,   -- 复用字段
    'bindphone' AS relation_source,
    MIN(bindding_time) AS first_bindding_time
FROM dwd_user_bindphone_log
WHERE user_id IS NOT NULL AND phone IS NOT NULL
GROUP BY user_id, phone;

生成OneID的方法:取最早注册的user_id作为统一标识。

-- 为每个设备找到关联的user_id,取最早的那个作为one_id
CREATE TABLE dim_id_mapping AS
SELECT
    device_id,
    user_id,
    FIRST_VALUE(user_id) OVER(
        PARTITION BY device_id
        ORDER BY first_bindding_time ASC
    ) AS one_id
FROM tmp_id_pairs;

优缺点:

  • 优点:准确率极高(接近100%),因为是基于确定事件的直接关联
  • 缺点:覆盖率有限——未登录的用户打不通,只在App外的行为(纯网页浏览)无法关联

我们出海业务大概80%的活跃用户能通过确定性匹配打通。剩下20%主要是"只安装没注册"的用户和"只看了广告没安装"的用户。

方案二:图算法打通(进阶)

image.png

当ID关系链比较复杂时,确定性匹配只能处理"直接关联",但有些关系是间接的。

举个例子:

设备A → user_id_1 → 手机号 138xxxx
设备B → user_id_2 → 手机号 138xxxx(同一个手机号!)

直接关联:设备A-user_id_1, 设备B-user_id_2
间接关联:通过手机号发现 user_id_1 和 user_id_2 是同一个人
→ 设备A、设备B、user_id_1、user_id_2 都应该映射到同一个one_id

这种间接关联用简单的SQL处理很麻烦,但用图算法就很自然:把所有ID当作图的节点,ID关系对当作边,求连通分量——同一个连通分量里的所有ID就是同一个自然人。

实现步骤:
1. 构建ID关系图
   - 节点:所有出现过的ID值(device_id、user_id、phone、openid...)
   - 边:每对有关联关系的ID之间连一条边

2. 求连通分量
   - 方法A:Spark GraphX 的 ConnectedComponents 算法
   - 方法B:自实现 Union-Find(并查集)算法
   - 方法C:SQL迭代(适合ID关系简单、规模不大的场景)

3. 分配OneID
   - 每个连通分量分配一个统一ID
   - 通常取分量内最早出现的user_id作为one_id

Spark GraphX实现的核心代码思路(伪代码):

// 构建ID对作为图的边
val edges = idPairs.map(row => Edge(row.id1_hash, row.id2_hash, 1))
val graph = Graph.fromEdges(edges, defaultValue = 0)

// 求连通分量
val cc = graph.connectedComponents()

// cc.vertices 包含 (节点ID, 所属连通分量ID)
// 同一个连通分量的所有ID就是同一个自然人

优缺点:

  • 优点:覆盖率高,能发现间接关联,比确定性匹配多打通10-20%的用户
  • 缺点:有误关联风险——如果公共设备(网吧电脑、家庭共享iPad)也参与打通,可能把不同的人连到一起

方案三:概率性匹配(特定场景补充)

image.png

通过设备指纹、IP地址、行为特征等弱信号推测是否是同一人。

判断条件(多个弱信号叠加):
- 相同IP地址
- 相同设备型号 + 操作系统版本
- 相似的使用时间段
- 相似的内容偏好
→ 多个信号综合打分,超过阈值判定为同一人

这种方法准确率较低(80%左右),一般只在以下场景使用:

  • 广告归因中的"模糊归因"(精确归因不到时的兜底)
  • 用户增长分析中的辅助参考
  • 不直接用于核心指标计算

image.png

实际项目中,绝大多数公司用的是方案一为主 + 方案二辅助的组合。概率性匹配因为准确率低,业务方不太信任,只在广告归因等特定场景使用。我们的做法也是这样。


OneID落地的关键设计

ID映射表结构

CREATE TABLE dim_id_mapping (
    one_id          BIGINT    COMMENT '统一用户ID',
    id_type         STRING    COMMENT 'ID类型:user_id/device_id/phone/openid/click_id',
    id_value        STRING    COMMENT 'ID原始值',
    relation_source STRING    COMMENT '关系来源:login/bindphone/attribution/graph',
    confidence      STRING    COMMENT '置信度:high(确定性匹配)/medium(图算法)/low(概率性)',
    first_bindding_time TIMESTAMP COMMENT '首次关联时间',
    last_active_time    TIMESTAMP COMMENT '最近活跃时间',
    is_valid        INT       COMMENT '是否有效:1=有效,0=已过期',
    dt              STRING    COMMENT '分区日期'
) COMMENT '统一用户ID映射表';

image.png

使用方式

所有下游分析统一通过one_id关联:

-- 用OneID统一计算真实DAU
SELECT COUNT(DISTINCT m.one_id) AS real_dau
FROM dwd_user_event_detail_di e
JOIN dim_id_mapping m
  ON e.device_id = m.id_value
  AND m.id_type = 'device_id'
  AND m.is_valid = 1
WHERE e.dt = '2025-01-15';

-- 广告归因:从广告点击到App付费的完整链路
SELECT
    ad.campaign_name,
    COUNT(DISTINCT m.one_id) AS attributed_users,
    SUM(p.pay_amount) AS attributed_revenue
FROM dwd_ad_click_detail_di ad
JOIN dim_id_mapping m1 ON ad.click_id = m1.id_value AND m1.id_type = 'click_id'
JOIN dim_id_mapping m2 ON m1.one_id = m2.one_id AND m2.id_type = 'user_id'
JOIN dwd_trade_pay_detail_di p ON m2.id_value = p.user_id
WHERE ad.dt >= '2025-01-08'
  AND p.dt >= '2025-01-08'
GROUP BY ad.campaign_name;

必须注意的三个坑

坑一:公共设备问题

image.png

一台网吧电脑或家庭共享iPad,可能被多个人使用。如果不加限制,一个device_id关联了50个user_id,这些用户全被合并成了"一个人"——DAU从50变成1,数据严重失真。

解决方案: 设置阈值规则。

-- 标记公共设备:一个设备关联超过10个用户就排除
WITH device_user_cnt AS (
    SELECT device_id, COUNT(DISTINCT user_id) AS user_cnt
    FROM tmp_id_pairs
    GROUP BY device_id
)
-- 排除公共设备
SELECT * FROM tmp_id_pairs
WHERE device_id NOT IN (
    SELECT device_id FROM device_user_cnt WHERE user_cnt > 10
);

我们的实际经验:阈值设为10效果比较好。太低会误杀家庭共享设备(一家3-4口人共用iPad),太高会让真正的公共设备漏网。

坑二:ID漂移

image.png

用户换手机了。旧手机的device_id和当前user_id的关系还在,但旧手机可能已经给了别人。如果旧手机的新主人也装了你的App,两个不同的人就通过这个device_id被关联在一起了。

解决方案: 设置关系有效期。

-- ID关系超过180天未活跃就标记为过期
UPDATE dim_id_mapping
SET is_valid = 0
WHERE DATEDIFF(CURRENT_DATE, last_active_time) > 180;

坑三:OneID合并冲突

image.png

两个原本独立的one_id,因为新发现的关系需要合并。比如用户A和用户B一直被认为是两个人,突然发现他们绑定了同一个手机号——需要把两个one_id合并为一个。

这时候所有引用了旧one_id的下游表都需要更新,这是一个很重的操作。

解决方案: 维护一张one_id合并历史表,下游表在计算时先查合并表做映射。


面试怎么答?

高分回答

「我们用的是确定性匹配为主的OneID方案。核心是一张ID映射表dim_id_mapping,存储user_id、device_id、phone、广告click_id等多种ID之间的映射关系。关系来源主要是登录事件和绑定事件——用户登录时同时上报了device_id和user_id,我们就自动建立映射。统一ID取最早注册的user_id。」

「实际落地遇到过两个问题:一是公共设备——一个iPad关联了50多个user_id,我们加了阈值规则,一个设备关联超过10个用户就标记为公共设备排除;二是ID过期——用户换手机后旧设备的映射关系需要失效,我们设了180天的有效期。当前方案的准确率在95%以上,覆盖了80%的活跃用户。」

追问防线

image.png

追问1:OneID的更新频率是什么?

我们是T+1天级更新。每天凌晨跑一个调度任务,从前一天的登录日志和绑定事件中提取新的ID关系对,增量更新到dim_id_mapping。

如果业务对实时性要求高(比如实时广告归因),可以用Flink做实时ID关系采集,新关系产生时立即写入。但大部分业务场景T+1已经够了。

追问2:隐私合规怎么处理?

这是出海业务的敏感点。手机号、设备ID这些属于个人隐私数据(PII),GDPR/CCPA等法规对其使用有严格要求。

我们的做法:一是ID值在存储时做哈希脱敏,dim_id_mapping里存的不是原始手机号而是哈希值;二是用户申请数据删除时,要能定位到所有关联ID并一并清除;三是ID映射关系只在数仓内部使用,不对外暴露。

追问3:如果不做OneID,有没有替代方案?

最简单的替代方案是强制登录——App的所有功能都需要登录才能使用,这样就不存在"未登录用户"的问题,device_id和user_id天然绑定。

但这在很多场景下不可行——比如用户首次打开App就要看到推荐内容,不可能先让人注册。所以OneID在大部分多端、多渠道的产品中都是必须的。


总结

image.png

  • OneID的目标:多端多渠道ID映射为统一用户标识,解决DAU虚高、归因断链、画像残缺等问题
  • 三种方案:确定性匹配(主力,准确率高)→ 图算法(补充,发现间接关联)→ 概率性匹配(特定场景兜底)
  • 核心表:dim_id_mapping,存ID类型+ID值+one_id+置信度+有效期
  • 三个必踩的坑:公共设备(设阈值排除)、ID漂移(设有效期)、合并冲突(维护合并历史)
  • 面试关键:说出你用的具体方案、遇到的问题、解决的办法和效果数据
刷题进度保存在当前浏览器