# 标签体系 > 本文档记录 `poyee-data-warehouse` TDM 层(主题域模型层)的标签架构设计。 > TDM 层采用**长表(明细)+ 宽表(服务)+ 人群包**三级结构,按实体类型分表。 > > **标签体系与指标体系是独立的两套体系**:指标回答"多少"(度量 + 聚合,住在 DWS/ADS),标签回答"是什么/哪一类"(分类 + 圈选,住在 TDM)。两者消费者不同、更新模式不同、计算逻辑不同,分开设计、各自演进。 ## 1. 整体架构 ``` 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 | | 宽表/报表 | | 圈选 + 交叉计算 | +-----------+ +---------------------+ ``` **设计原则**: - 按**实体类型**分表(用户/商品/商家),不按标签主题拆表 - 长表负责**全量明细存储**,宽表负责**高频服务**,人群包负责**圈选计算** - 前期只用 Hive 做离线标签计算,不引入额外组件 ## 2. 标签明细长表(EAV 结构) 每个实体类型一张长表,所有标签值纵向存放。新增标签只需插入新行,不改表结构。 **表清单**: | 表名 | 实体 | 更新策略 | |------|------|---------| | `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 | 分区日期 | **优势**: - 新增标签零 DDL 变更,只需在标签计算 SQL 中增加 INSERT 行 - 所有标签统一口径管理,便于建设标签字典元数据 - 支持按 `tag_code` 灵活查询任意标签组合 ## 3. 核心标签宽表 从长表 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 模式**: ```sql -- 从长表 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 ; ``` **宽表字段准入标准**: - 下游 >= 2 个 ADS 表或 BI 看板引用 - 标签计算口径已稳定(不处于频繁调整期) - 非核心 / 低频标签保留在长表,按需从长表取 ## 4. 人群包 以圈选规则为主键,存储实体 ID 集合,支持人群圈选和二次计算(交集、并集、差集)。 **阶段 1 为单表设计**:所有人群包存入一张表 `tdm_usr_crowd_ful_d`,每行一个人群(`crowd_code` 区分)。阶段 2+ 存储格式从 `ARRAY` 升级为 `BINARY`(bitmap),但表结构不变。详见 §6 演进路线。 ## 5. 标签字典 所有 `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 / 文档维护,后期可建元数据管理模块。 ## 6. 标签服务演进路线 前期不引入新组件,Hive 承担离线标签计算的全部职责。后续按**实际瓶颈**逐阶段引入新能力,不超前建设。 ```mermaid graph TB subgraph "阶段 1 - Hive 7 表" L1[3 长表 + 3 宽表] C1["1 人群包
ARRAY BIGINT 存 ID 列表"] end subgraph "阶段 2 - Bitmap UDF" C2["人群包改用 BINARY
RoaringBitmap 序列化
自研 UDF 做交并差"] end subgraph "阶段 3 - HBase 在线服务" HB["宽表同步至 HBase
毫秒级单实体标签查询"] end subgraph "阶段 4 - ClickHouse 人群计算" CK["人群包迁移至 ClickHouse
原生 bitmap 函数
高性能交叉计算"] end subgraph "阶段 5 - ES 检索型画像" ES["宽表同步至 ES
多条件组合筛选
模糊匹配画像检索"] end L1 --> C1 C1 -->|"人群包 >50 个
或交叉计算 >3 层"| C2 C2 -->|"业务需要实时
标签查询"| HB C2 -->|"Hive bitmap UDF
性能不足"| CK HB -->|"需要反向检索
按标签条件找人"| ES ``` ### 阶段 1:Hive 7 表(当前) 纯 Hive / Spark,零额外组件。 | 表 | 数量 | 存储 | 说明 | |----|:----:|------|------| | 标签明细长表 | 3 | Hive ORC | `tdm_{usr|prd|shp}_tag_ful_d`,EAV 结构,所有标签纵向存放 | | 核心标签宽表 | 3 | Hive ORC | `tdm_{usr|prd|shp}_profile_ful_d`,从长表 pivot 高频标签 | | 人群包 | 1 | Hive ORC | `tdm_usr_crowd_ful_d`,用 `ARRAY` 存实体 ID 列表 | **人群包表结构(阶段 1)**: | 字段 | 类型 | 说明 | |------|------|------| | `crowd_code` | STRING | 人群编码(如 `high_value`、`churn_risk`) | | `crowd_name` | STRING | 人群中文名 | | `crowd_rule` | STRING | 圈选规则描述(便于审计) | | `member_ids` | ARRAY\ | 实体 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` 存百万级 ID 列表时,单行数据量大,序列化/反序列化慢 - `array_intersect` 是 O(n*m) 复杂度,两个百万级数组交叉计算耗时严重 - 不支持跨行的集合运算(两个人群包做交集需要先 `LATERAL VIEW EXPLODE` 再 JOIN,性能差) ### 阶段 2:Bitmap UDF **触发条件**:人群包数量 > 50 个,或出现 >= 3 层交叉计算需求(如"高价值 ∩ 活跃 ∩ 非流失")。 **改造内容**: - 人群包表 `member_ids` 字段从 `ARRAY` 改为 `BINARY`(RoaringBitmap 序列化) - 自研 Spark UDF(Java/Scala,注册到 `dw_base/spark/udf/`): | UDF | 功能 | 对应 SQL | |-----|------|---------| | `bitmap_create(array)` | 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 个数量级。 ### 阶段 3:HBase 在线标签服务 **触发条件**:业务侧需要实时标签查询(推荐系统、风控引擎、客服工作台等需要毫秒级响应单用户画像)。 **改造内容**: - Hive TDM 仍然是离线标签计算的唯一来源,计算逻辑不变 - 新增同步链路:宽表 `tdm_{usr|prd|shp}_profile_ful_d` 每日计算完成后,通过 BulkLoad 同步到 HBase - HBase 表设计:RowKey = `{entity_type}_{entity_id}`,Column Family 按标签分类,Qualifier = 标签字段名 - 业务侧通过 HBase Client / Thrift API 查询,Hive 侧不受影响 ``` Hive TDM 宽表 --BulkLoad--> HBase(在线读,毫秒级) | +--> ADS(离线报表,不变) ``` **要点**:HBase 只做**读服务**,不做标签计算。标签口径和计算逻辑的唯一来源始终是 Hive TDM 长表。 ### 阶段 4:ClickHouse 人群包计算服务 **触发条件**:人群包数量多(> 200)且交叉计算复杂(> 5 层嵌套),Hive bitmap UDF 的批处理模式无法满足运营侧"秒级出圈选结果"的交互需求。 **改造内容**: - 人群包存储和交叉计算从 Hive 迁移至 ClickHouse - ClickHouse 提供原生 bitmap 函数(`bitmapAnd`、`bitmapOr`、`bitmapXor`、`bitmapCardinality`、`bitmapContains`),无需自研 UDF - Hive TDM 仍然负责标签计算,计算完成后将人群包通过 DataX / JDBC 写入 ClickHouse - 运营侧圈选请求直接查 ClickHouse,秒级返回 ``` Hive TDM(离线标签计算,唯一来源) +--> ADS(报表/分析) +--> HBase(在线标签服务,单实体查询) +--> ClickHouse(人群包存储 + bitmap 交叉计算,秒级圈选) ``` **要点**:ClickHouse 只承接人群包的**存储和集合运算**,标签本身的计算口径仍由 Hive TDM 长表统一管理,避免出现两套计算逻辑。 ### 阶段 5:Elasticsearch 检索型画像服务 **触发条件**:业务侧需要**按标签条件反向检索实体**(如"找出所有城市=上海、年龄>25、偏好运动=篮球的用户"),HBase 只支持 RowKey 点查,无法满足多条件组合筛选和模糊匹配。 **改造内容**: - 宽表 `tdm_{usr|prd|shp}_profile_ful_d` 每日计算完成后,同步写入 ES(通过 DataX hdfs-elasticsearch 或 Spark ES connector) - ES 索引设计:每个实体类型一个索引(`tdm_usr_profile` / `tdm_prd_profile` / `tdm_shp_profile`),文档 ID = entity_id,字段映射与宽表一致 - 运营/产品侧通过 ES 查询接口做多条件组合筛选、范围查询、模糊匹配 **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` | 宽表 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 只做下游的读服务和集合运算,不做标签计算。 ## 7. 相关文档 - [数仓分层与建模](20-数仓分层与建模.md) -- TDM 层定位 - [命名规范](21-命名规范.md) -- TDM 层命名规则(§3.6) - [指标体系](22-指标体系.md) -- 指标与标签是独立体系,口径各自管理