本文档记录
poyee-data-warehouseTDM 层(主题域模型层)的标签架构设计。 TDM 层采用长表(明细)+ 宽表(服务)+ 人群包三级结构,按实体类型分表。标签体系与指标体系是独立的两套体系:指标回答"多少"(度量 + 聚合,住在 DWS/ADS),标签回答"是什么/哪一类"(分类 + 圈选,住在 TDM)。两者消费者不同、更新模式不同、计算逻辑不同,分开设计、各自演进。
DWD / DWS(事实 + 维度)
|
v
+---------------------------------------------+
| TDM 标签明细长表(EAV) |
| tdm_usr_tag_ful_d |
| tdm_prd_tag_ful_d |
| tdm_shp_tag_ful_d |
| 所有标签纵向存放,灵活扩展,不改表结构 |
+--------------+------------------------------+
| pivot 高频核心标签
v
+---------------------------------------------+
| TDM 核心标签宽表 |
| tdm_usr_profile_ful_d |
| tdm_prd_profile_ful_d |
| tdm_shp_profile_ful_d |
| 高频标签横向展开,下游 ADS / BI 直接 JOIN |
+--------------+------------------------------+
|
+--------+--------+
v v
+-----------+ +---------------------+
| ADS 层 | | TDM 人群包 |
| 主题标签 | | tdm_usr_crowd_ful_d |
| 宽表/报表 | | 圈选 + 交叉计算 |
+-----------+ +---------------------+
设计原则:
每个实体类型一张长表,所有标签值纵向存放。新增标签只需插入新行,不改表结构。
表清单:
| 表名 | 实体 | 更新策略 |
|---|---|---|
tdm_usr_tag_ful_d |
用户 | 全量重刷,日 |
tdm_prd_tag_ful_d |
商品 | 全量重刷,日 |
tdm_shp_tag_ful_d |
商家 | 全量重刷,日 |
典型字段结构:
| 字段 | 类型 | 说明 |
|---|---|---|
entity_id |
BIGINT | 实体 ID(user_id / product_id / shop_id) |
tag_code |
STRING | 标签编码(全局唯一,如 usr_gender、usr_pref_sport) |
tag_value |
STRING | 标签值(统一 STRING,下游按需转型) |
tag_type |
STRING | 标签类型(categorical / numeric / boolean / json) |
confidence |
DECIMAL(5,4) | 置信度(0~1,规则标签填 1.0,模型标签填模型输出) |
etl_time |
TIMESTAMP | ETL 写入时间 |
dt |
STRING | 分区日期 |
优势:
tag_code 灵活查询任意标签组合从长表 pivot 高频、核心标签为列,供下游 ADS 和 BI 直接 JOIN,避免每次都对长表做条件聚合。
表清单:
| 表名 | 实体 | 更新策略 |
|---|---|---|
tdm_usr_profile_ful_d |
用户 | 全量重刷,日 |
tdm_prd_profile_ful_d |
商品 | 全量重刷,日 |
tdm_shp_profile_ful_d |
商家 | 全量重刷,日 |
用户宽表候选字段(示例):
| 字段 | 类型 | 来源标签 |
|---|---|---|
user_id |
BIGINT | 主键 |
gender |
STRING | usr_gender |
age |
INT | usr_age |
city |
STRING | usr_city |
reg_days |
INT | usr_reg_days |
pref_sport |
STRING | usr_pref_sport(Top 1 偏好运动) |
pref_product |
STRING | usr_pref_product(Top 1 偏好商品) |
user_lvl |
STRING | usr_value_level(高/中/低价值) |
is_churn_risk |
BOOLEAN | usr_churn_risk |
etl_time |
TIMESTAMP | ETL 写入时间 |
dt |
STRING | 分区日期 |
长表 -> 宽表的 ETL 模式:
-- 从长表 pivot 到宽表(Spark SQL CASE WHEN 模式)
INSERT OVERWRITE TABLE tdm.tdm_usr_profile_ful_d PARTITION (dt='${dt}')
SELECT
entity_id AS user_id
,MAX(CASE WHEN tag_code = 'usr_gender' THEN tag_value END) AS gender
,MAX(CASE WHEN tag_code = 'usr_age' THEN CAST(tag_value AS INT) END) AS age
,MAX(CASE WHEN tag_code = 'usr_city' THEN tag_value END) AS city
-- ... 其他核心标签
,CURRENT_TIMESTAMP() AS etl_time
FROM tdm.tdm_usr_tag_ful_d
WHERE dt = '${dt}'
GROUP BY entity_id
;
宽表字段准入标准:
以圈选规则为主键,存储实体 ID 集合,支持人群圈选和二次计算(交集、并集、差集)。
阶段 1 为单表设计:所有人群包存入一张表 tdm_usr_crowd_ful_d,每行一个人群(crowd_code 区分)。阶段 2+ 存储格式从 ARRAY<BIGINT> 升级为 BINARY(bitmap),但表结构不变。详见 §6 演进路线。
所有 tag_code 必须在标签字典中注册后才能写入长表,确保编码唯一、口径明确。
| 字段 | 说明 |
|---|---|
tag_code |
全局唯一编码,格式 {实体}_{标签名}(如 usr_gender、prd_price_band) |
tag_name |
中文名称 |
entity_type |
usr / prd / shp |
tag_type |
categorical / numeric / boolean / json |
compute_logic |
计算口径(SQL 或自然语言) |
source_table |
上游依赖表 |
update_cycle |
更新周期 |
owner |
负责人 |
is_core |
是否进入宽表 |
标签字典前期用 Docmost / 文档维护,后期可建元数据管理模块。
前期不引入新组件,Hive 承担离线标签计算的全部职责。后续按实际瓶颈逐阶段引入新能力,不超前建设。
graph TB
subgraph "阶段 1 - Hive 7 表"
L1[3 长表 + 3 宽表]
C1["1 人群包<br/>ARRAY BIGINT 存 ID 列表"]
end
subgraph "阶段 2 - Bitmap UDF"
C2["人群包改用 BINARY<br/>RoaringBitmap 序列化<br/>自研 UDF 做交并差"]
end
subgraph "阶段 3 - HBase 在线服务"
HB["宽表同步至 HBase<br/>毫秒级单实体标签查询"]
end
subgraph "阶段 4 - ClickHouse 人群计算"
CK["人群包迁移至 ClickHouse<br/>原生 bitmap 函数<br/>高性能交叉计算"]
end
subgraph "阶段 5 - ES 检索型画像"
ES["宽表同步至 ES<br/>多条件组合筛选<br/>模糊匹配画像检索"]
end
L1 --> C1
C1 -->|"人群包 >50 个<br/>或交叉计算 >3 层"| C2
C2 -->|"业务需要实时<br/>标签查询"| HB
C2 -->|"Hive bitmap UDF<br/>性能不足"| CK
HB -->|"需要反向检索<br/>按标签条件找人"| ES
纯 Hive / Spark,零额外组件。
| 表 | 数量 | 存储 | 说明 |
|---|---|---|---|
| 标签明细长表 | 3 | Hive ORC | `tdm_{usr |
| 核心标签宽表 | 3 | Hive ORC | `tdm_{usr |
| 人群包 | 1 | Hive ORC | tdm_usr_crowd_ful_d,用 ARRAY<BIGINT> 存实体 ID 列表 |
人群包表结构(阶段 1):
| 字段 | 类型 | 说明 |
|---|---|---|
crowd_code |
STRING | 人群编码(如 high_value、churn_risk) |
crowd_name |
STRING | 人群中文名 |
crowd_rule |
STRING | 圈选规则描述(便于审计) |
member_ids |
ARRAY<BIGINT> | 实体 ID 列表 |
member_cnt |
BIGINT | 人群人数(冗余,避免每次 SIZE(member_ids)) |
etl_time |
TIMESTAMP | ETL 写入时间 |
dt |
STRING | 分区日期 |
圈选方式:宽表 WHERE 条件过滤 -> COLLECT_LIST(user_id) 写入人群包。交叉计算用 array_intersect / array_union(Spark 2.4 内置)。
阶段 1 的局限:
ARRAY<BIGINT> 存百万级 ID 列表时,单行数据量大,序列化/反序列化慢array_intersect 是 O(n*m) 复杂度,两个百万级数组交叉计算耗时严重LATERAL VIEW EXPLODE 再 JOIN,性能差)触发条件:人群包数量 > 50 个,或出现 >= 3 层交叉计算需求(如"高价值 ∩ 活跃 ∩ 非流失")。
改造内容:
member_ids 字段从 ARRAY<BIGINT> 改为 BINARY(RoaringBitmap 序列化)dw_base/udf/):| UDF | 功能 | 对应 SQL |
|---|---|---|
bitmap_create(array<bigint>) |
ID 列表 -> bitmap | 写入时调用 |
bitmap_and(binary, binary) |
交集 | 人群 A ∩ B |
bitmap_or(binary, binary) |
并集 | 人群 A ∪ B |
bitmap_andnot(binary, binary) |
差集 | 人群 A - B |
bitmap_count(binary) |
计数 | 人群人数 |
bitmap_contains(binary, bigint) |
判断是否包含某 ID | 单用户判定 |
bitmap_to_array(binary) |
bitmap -> ID 列表 | 导出明细 |
优势:RoaringBitmap 对整型集合压缩率高(百万 ID 通常 < 1MB),交并差都是 O(n) 位运算,比数组快 2-3 个数量级。
触发条件:业务侧需要实时标签查询(推荐系统、风控引擎、客服工作台等需要毫秒级响应单用户画像)。
改造内容:
tdm_{usr|prd|shp}_profile_ful_d 每日计算完成后,通过 BulkLoad 同步到 HBase{entity_type}_{entity_id},Column Family 按标签分类,Qualifier = 标签字段名业务侧通过 HBase Client / Thrift API 查询,Hive 侧不受影响
Hive TDM 宽表 --BulkLoad--> HBase(在线读,毫秒级)
|
+--> ADS(离线报表,不变)
要点:HBase 只做读服务,不做标签计算。标签口径和计算逻辑的唯一来源始终是 Hive TDM 长表。
触发条件:人群包数量多(> 200)且交叉计算复杂(> 5 层嵌套),Hive bitmap UDF 的批处理模式无法满足运营侧"秒级出圈选结果"的交互需求。
改造内容:
bitmapAnd、bitmapOr、bitmapXor、bitmapCardinality、bitmapContains),无需自研 UDF运营侧圈选请求直接查 ClickHouse,秒级返回
Hive TDM(离线标签计算,唯一来源)
+--> ADS(报表/分析)
+--> HBase(在线标签服务,单实体查询)
+--> ClickHouse(人群包存储 + bitmap 交叉计算,秒级圈选)
要点:ClickHouse 只承接人群包的存储和集合运算,标签本身的计算口径仍由 Hive TDM 长表统一管理,避免出现两套计算逻辑。
触发条件:业务侧需要按标签条件反向检索实体(如"找出所有城市=上海、年龄>25、偏好运动=篮球的用户"),HBase 只支持 RowKey 点查,无法满足多条件组合筛选和模糊匹配。
改造内容:
tdm_{usr|prd|shp}_profile_ful_d 每日计算完成后,同步写入 ES(通过 DataX hdfs-elasticsearch 或 Spark ES connector)tdm_usr_profile / tdm_prd_profile / tdm_shp_profile),文档 ID = entity_id,字段映射与宽表一致HBase vs ES 职责分工:
| 场景 | 组件 | 示例 |
|---|---|---|
| 正向点查:已知实体 ID,取全部标签 | HBase | 推荐系统传入 user_id,取用户画像 |
| 反向检索:按标签条件组合筛选实体 | ES | 运营圈选"上海+25岁以上+篮球爱好者" |
要点:ES 和 HBase 都只做读服务,数据来源是同一份 Hive TDM 宽表,不存在口径分歧。
完整数据流(阶段 5):
Hive TDM(离线标签计算,唯一来源)
+--> ADS(报表/分析)
+--> HBase(在线点查:已知 ID 取标签)
+--> ES(检索型画像:按标签条件找实体)
+--> ClickHouse(人群包 bitmap 交叉计算,秒级圈选)
| 阶段 | 组件 | TDM 表 | 人群包存储 | 圈选能力 | 在线查询 |
|---|---|---|---|---|---|
| 1 | Hive | 3 长 + 3 宽 + 1 人群 | ARRAY<BIGINT> |
宽表 WHERE + COLLECT_LIST | 无 |
| 2 | Hive + bitmap UDF | 同上 | BINARY(RoaringBitmap) |
bitmap 交并差,2-3 数量级提速 | 无 |
| 3 | +HBase | 同上 | 同阶段 2 | 同阶段 2 | HBase 点查(ID -> 标签) |
| 4 | +ClickHouse | 同上 | ClickHouse 原生 bitmap | 秒级交互式圈选 | HBase(点查)+ ClickHouse(圈选) |
| 5 | +ES | 同上 | 同阶段 4 | 同阶段 4 | HBase(点查)+ ES(反向检索)+ ClickHouse(圈选) |
核心不变量:无论演进到哪个阶段,标签的计算口径和计算逻辑始终由 Hive TDM 长表统一管理。HBase / ES / ClickHouse 只做下游的读服务和集合运算,不做标签计算。