ChatBI 语义层设计详解 --- 表结构与设计合理性
基于电商数仓实际表结构 · 12 张元数据表 · MySQL JSON + 应用层 cosine 向量检索
配套文档:chatbi_semantic_layer_design.md(原始设计文档)| chatbi_semantic_layer_design_explained.html(HTML 富媒体版)
目录
- 六层分层架构总览
- [元数据 ER 关系图](#元数据 ER 关系图)
- [核心配置表设计详解(5 张表)](#核心配置表设计详解(5 张表))
- [辅助配置表设计详解(3 张表)](#辅助配置表设计详解(3 张表))
- [向量检索表设计详解(2 张表)](#向量检索表设计详解(2 张表))
- [协作治理表设计详解(3 张表)](#协作治理表设计详解(3 张表))
- [配置拆分映射:一条 YAML 如何拆到多表](#配置拆分映射:一条 YAML 如何拆到多表)
- 设计原则总结
1. 六层分层架构总览
语义层不是一层独立的"表",而是介于物理数仓(DIM/DWD/DWS/ADS)与用户交互之间的一组 结构化配置 + 确定性编译引擎。整体从底向上共 6 层:

编译器 7 步流程 :S0 加载(5min缓存) → S1 校验 → S2 选表合并 → S3 原子建列 → S4 派生展开 → S5 补 Join → S6 补 Where → S7 GroupBy/OrderBy
设计合理性:将"意图理解"(LLM 擅长)和"SQL 编译"(确定性逻辑)彻底分离。LLM 只输出 DSL JSON(选词填空),SQL 的 Join 路径、口径过滤、字段映射全部由语义层引擎确定性处理,避免 LLM 产生幻觉 SQL。
2. 元数据 ER 关系图
12 张表以 sl_ 为前缀,存独立元数据库 chatbi_meta,与业务数仓物理隔离。双核心辐射结构:sl_metric(指标)和 sl_dimension(维度)。

三层关注点分组
| 分组 | 表 | 职责 |
|---|---|---|
| 维度侧 | sl_dimension、sl_dimension_hierarchy | 维度定义与层次 |
| 指标侧 | sl_metric、sl_synonym、sl_metric_caliber、sl_metric_dim_avail | 指标定义、同义词、口径、可用维度 |
| 辅助配置 | sl_join_path、sl_global_filter | Join 路径、全局过滤器(共享配置) |
| 向量检索 | sl_vector_doc、sl_vector_doc_dead_letter | 向量文档与死信补偿 |
| 协作治理 | sl_user_role、sl_change_request、sl_audit_log | 角色权限、审批状态机、审计回滚 |
3. 核心配置表设计详解(5 张表)
这 5 张表构成语义层的"业务核心配置",定义了"有哪些指标、有哪些维度、它们之间如何关联、叫什么名字、口径是什么"。
3.1 sl_metric --- 指标主表
核心表 · 47+ 个指标的元定义
设计思路 :每个指标一行,记录其 物理映射 (preferred_table + physical_expr)和 回退路径(fallback_table/where/expr)。同时区分原子指标(atomic)和派生指标(derived,通过 formula + components 引用其他指标)。
具体例子:pay_gmv(成交GMV)
# 首选 DWS 预聚合表(80% 场景命中这里)
metric_id: pay_gmv
preferred_table: dws_order_day # 已按天+渠道预聚合
physical_expr: pay_gmv # 直接 SUM(pay_gmv) 即可
# 回退 DWD 明细(如用户要按 SKU 下钻,DWS 没有 sku_id 时)
fallback_table: dwd_order_info
fallback_where: 'pay_status="已支付"'
fallback_expr: order_real_amount # COUNT/SUM 用此字段
# 派生指标(如 avpu 客单价)
metric_type: derived
formula: 'pay_gmv / pay_user_uv'
components: '["pay_gmv","pay_user_uv"]' # 编译器先算子指标再做除法
合理性分析:
- 首选/回退分离:80% 查询命中 DWS(快),20% 回退 DWD(全),兼顾性能与覆盖面
- 派生指标公式化:avpu = pay_gmv / pay_user_uv,编译器自动拆成两个 CTE 再 JOIN,而非硬编码 SQL
- version 字段:配合审计日志支持版本回滚,每次修改 version+1
3.2 sl_dimension --- 维度主表
核心表 · 8 个分析维度
设计思路 :每个维度一行,记录其 物理字段 (physical_column)、关联维度表 (join_table + join_key)和 展示字段(display_column)。支持退化维度(join_table 为空)和需要 Join 的维度。
具体例子
# channel 维度:需要 Join dim_channel 拿渠道名
dim_id: channel
physical_column: channel_id # 事实表里的字段
join_table: dim_channel # 要 Join 的维度表
join_key: channel_id # Join 键
display_column: channel_name # 展示给用户看的字段
# date 维度:退化维度,不需要 Join(dt 已在事实表里)
dim_id: date
physical_column: dt
join_table: NULL # 退化维度
合理性分析:
- 退化维度处理:date 维度不需要 Join,join_table 为空,编译器自动跳过 Join 步骤
- 展示字段分离:事实表存 channel_id(整数),展示时 Join 拿 channel_name(中文),避免冗余存储
- sort_order:控制维度在 UI 上的展示顺序
3.3 sl_dimension_hierarchy --- 维度层次表
核心表 · 支持下钻/上卷
设计思路:一个维度有多个层次(如商品维度有 cate1/cate2/cate3/brand),拆到独立表而非存 JSON 数组,便于按层次查询和扩展。
具体例子:goods(商品)维度的层次
| dim_id | hierarchy | expr_sql | 含义 |
|---|---|---|---|
| goods | cate1 | cate1 | 一级类目下钻 |
| goods | cate2 | cate2 | 二级类目下钻 |
| goods | cate3 | cate3 | 三级类目下钻 |
| goods | brand | brand_name | 按品牌下钻 |
合理性 :如果将层次存在 sl_dimension 的 JSON 字段里,查询"哪些维度支持 cate1 层次"需要全表扫描 JSON。拆成独立表后,一条
SELECT dim_id FROM sl_dimension_hierarchy WHERE hierarchy='cate1'即可,且新增层次不影响主表。
3.4 sl_synonym --- 同义词映射表
核心表 · 业务术语→指标/维度ID
设计思路 :业务方用不同名字称呼同一指标(GMV/成交额/交易额/总销售额),独立成表而非存 sl_metric 的 synonyms JSON 字段。关键:支持多人各自补充同义词。
具体例子:pay_gmv 的同义词
| term | target_type | target_id | created_by |
|---|---|---|---|
| GMV | metric | pay_gmv | xiaofei |
| 成交额 | metric | pay_gmv | xiaofei |
| 交易额 | metric | pay_gmv | lisi |
| 销售金额 | metric | pay_gmv | lisi |
| 总销售额 | metric | pay_gmv | wangwu |
合理性 :如果存在 sl_metric.synonyms JSON 字段里,多人修改同一指标的同义词会产生 行锁冲突 (xiaofei 加"成交额"时,lisi 无法同时加"交易额")。独立成表后,每人各插一行,无锁冲突。同时
UNIQUE(term, target_type)保证同一术语不重复映射到不同目标。
3.5 sl_metric_caliber --- 口径冲突仲裁表
口径表 · 同一指标多口径来源
设计思路 :同一个"GMV"在不同报表里有不同口径(下单GMV vs 成交GMV vs 老报表废弃口径),需要明确 主推口径(primary) 和 废弃口径(deprecated),避免数据不一致。
具体例子:pay_gmv 的口径冲突
| metric_id | caliber_type | table_name | definition | confirmed |
|---|---|---|---|---|
| pay_gmv | primary | dws_order_day.pay_gmv | 已支付订单实付总金额 | 1 |
| pay_gmv | deprecated | ads_old_report.gmv_amt | 老报表ADS口径(重复计算优惠券) | 0 |
合理性 :当业务方问"GMV是多少"时,编译器查
sl_metric_caliber WHERE metric_id='pay_gmv' AND caliber_type='primary'确定使用 dws_order_day 口径。deprecated 口径标注deprecate_until截止日,到期后可清理。需 approver 审批确认(approved_by 字段),防止随意修改口径。
4. 辅助配置表设计详解(3 张表)
这 3 张表是 共享配置,不被某个指标独占,而是被多个指标引用。
4.1 sl_metric_dim_avail --- 指标可用维度白名单
关联表 · 指标×维度 多对多
设计思路 :不是所有指标都能按所有维度下钻(如 GMV 不能按 SKU 下钻,因为 DWS 层没有 sku_id)。用关联表记录 允许/显式禁用 的维度组合。
具体例子:pay_gmv 的可用维度
| metric_id | dim_id | hierarchy | allowed | 说明 |
|---|---|---|---|---|
| pay_gmv | date | NULL | ✅ 1 | 支持按日期 |
| pay_gmv | channel | NULL | ✅ 1 | 支持按渠道 |
| pay_gmv | user | level | ✅ 1 | 支持按会员等级 |
| pay_gmv | sku | NULL | ❌ 0 | 显式禁用:DWS无sku_id |
合理性 :编译器 S1 校验阶段会检查
SELECT allowed FROM sl_metric_dim_avail WHERE metric_id=? AND dim_id=?。若 allowed=0,直接返回友好错误"指标成交GMV暂不支持按SKU维度下钻",而非生成错误 SQL。比在 sl_metric 里存 available_dims JSON 数组更利于查询"某维度下有哪些可用指标"。
4.2 sl_join_path --- Join 路径图谱
关联表 · 事实表↔维度表 Join 规则
设计思路 :编译器不靠 LLM 拼 Join,而是查这张表 确定性添加 JOIN。以事实表为粒度(非指标粒度),因为同一张事实表的所有指标共用同一套 Join 路径。
具体例子:dws_order_day 的 Join 路径
| fact_table | dim_table | join_type | on_keys |
|---|---|---|---|
| dws_order_day | dim_channel | LEFT | "channel_id" |
| dws_order_day | dim_date | LEFT | "dt" |
| dwd_order_detail | dwd_order_info | INNER | "order_id" |
| dwd_order_detail | dim_goods | LEFT | "spu_id" |
合理性 :以 表 而非 指标 为粒度,因为 pay_gmv、pay_user_uv、pay_order_cnt 三个指标都在 dws_order_day 上,只需查一次 Join 路径就能复用。避免在每个指标定义里重复写 Join 信息。
on_keys用 JSON 数组支持复合键(如 dws_ad_day 需 "channel_id","plan_id")。
4.3 sl_global_filter --- 全局口径过滤器
关联表 · 跨指标共享的过滤条件
设计思路 :有些过滤条件是全局通用的(排除测试数据、仅统计已支付),不应在每个指标里重复定义。指标通过 global_filters 字段引用这些共享过滤器的 name。
具体例子
# 全局过滤器定义
- name: exclude_test
sql_where: 'user_id not like "test_%" and order_id not like "TEST%"'
- name: pay_success
sql_where: 'pay_status = "已支付"'
# 指标引用(YAML 配置)
pay_gmv:
global_filters: ["exclude_test", "pay_success"]
# 编译器 S6 阶段自动追加 WHERE 条件
合理性 :修改"排除测试数据"的规则只需改一处,所有引用该过滤器的指标自动生效。
enabled字段支持一键关闭某过滤器(如调试时暂时不排除测试数据)。
5. 向量检索表设计详解(2 张表)
方案A:MySQL JSON 列存 768 维向量 + Java 应用层 cosine 相似度计算。向量文档是 派生数据,由业务表变更事件异步生成。
5.1 sl_vector_doc --- 向量文档表
向量表 · 语义检索的索引数据
设计思路 :将指标/维度的语义信息拼接成文档文本,调用 Embedding 模型生成 768 维向量,存入 JSON 列。检索时全表读取 + Java 层 cosine 计算 + Top-K 排序。source_id 逻辑关联 metric_id/dim_id,不做物理外键。
具体例子:pay_gmv 的向量文档
{
"doc_id": 1,
"source_type": "metric",
"source_id": "pay_gmv",
"domain": "transaction",
"doc_text": "成交GMV/GMV/成交额/交易额/总销售额: 已支付订单实付总金额。支持按date/channel/user.level/promotion下钻。口径=SUM(order_real_amount) where pay_status=已支付。",
"embedding": "[0.012,-0.034,0.056,...]",
"doc_version": 3
}
检索流程

合理性 :向量文档是 派生数据 ,源数据在 sl_metric/sl_synonym 等业务表里。用 source_id 逻辑关联而非物理外键,是因为向量文档允许短暂不一致(异步生成 + 死信重试)。
(source_type, source_id)唯一约束保证不重复,(source_type, domain)索引支持按域预过滤。
5.2 sl_vector_doc_dead_letter --- 死信表
向量表 · 向量生成失败的补偿
设计思路:向量生成依赖外部 Embedding API(阿里云百炼 text-embedding-v2),可能失败。失败记录入死信表,定时任务重试,保证最终一致性。
具体例子
{
"source_type": "metric",
"source_id": "pay_gmv",
"error_msg": "connect timeout to dashscope.aliyuncs.com",
"status": "PENDING",
"retry_count": 0
}
# 定时任务每 10 秒扫描,最多重试 3 次
SELECT ... WHERE status='PENDING' FETCH FIRST 50 ROWS ONLY
成功 → 重建 sl_vector_doc → 死信标记 SUCCESS
失败 → retry_count+1 → 超过 3 次标记 FAILED
合理性 :向量生成是 非关键路径(不影响业务数据写入),用死信表解耦。即使 Embedding API 宕机,业务表正常写入,待 API 恢复后死信重试补齐向量,保证最终一致。
6. 协作治理表设计详解(3 张表)
多人维护语义层时,需要 角色权限 、审批流程 、审计回滚 三件套。这是 YAML 文件方案无法实现的核心能力。
6.1 sl_user_role --- 用户角色表
治理表 · 4 类角色 + 域隔离
设计思路 :4 类角色(viewer/editor/approver/admin),通过 domain_scope JSON 字段限制 editor 只能改自己负责的业务域,防止跨域误改。
具体例子
| user_id | username | role | domain_scope | can_approve |
|---|---|---|---|---|
| xiaofei | 消费分析师 | editor | "traffic","conversion" | 0 |
| lisi | 交易数据BP | editor | "transaction","goods" | 0 |
| zhangsan | 数据负责人 | admin | NULL | 1 |
| wangwu | 业务方 | viewer | NULL | 0 |
权限矩阵
| 角色 | 查看 | 新增/编辑 | 删除 | 审批口径 | 用户管理 |
|---|---|---|---|---|---|
| viewer 只读 | ✅ | ❌ | ❌ | ❌ | ❌ |
| editor 编辑者 | ✅ | ✅ 仅自己域 | 仅自己新增的 | ❌ | ❌ |
| approver 审批者 | ✅ | ✅ | 受限 | ✅ | ❌ |
| admin 超管 | ✅ | ✅ 全部 | ✅ | ✅ | ✅ |
合理性 :xiaofei 只能改 traffic/conversion 域的指标(如 UV、CTR),改不了 transaction 域的 pay_gmv。查询时 SQL 自动加
WHERE domain IN (用户域列表),从数据层隔离权限。
6.2 sl_change_request --- 审批状态机表
治理表 · DRAFT→PENDING→APPROVED→APPLIED
设计思路:口径变更必须走审批流,防止"未审批先上线"。editor 提交申请(DRAFT→PENDING),approver 审批(PENDING→APPROVED/REJECTED),apply_worker 独立进程写入目标表(APPROVED→APPLIED)。
具体例子:修改 pay_gmv 口径
// editor xiaofei 提交变更申请
{
"entity_type": "metric",
"entity_id": "pay_gmv",
"change_type": "UPDATE",
"change_diff": "{\"preferred_table\":[\"dws_order_day\",\"ads_ec_overview_day\"]}",
"change_note": "改用 ADS 层口径,支持按 user 下钻",
"status": "PENDING",
"submitter_id": "xiaofei"
}
// approver zhangsan 审批通过
{
"status": "APPROVED",
"approver_id": "zhangsan"
}
// apply_worker 10秒轮询,写入 sl_metric
{
"status": "APPLIED",
"apply_note": "已更新 sl_metric,缓存已失效"
}
状态机流转
DRAFT 草稿 → PENDING 待审 → APPROVED 审批通过 → APPLIED 已生效
↓
REJECTED 已拒绝(附 reject_reason)
合理性 :editor 不能直接 UPDATE sl_metric,必须走状态机。apply_worker 独立进程 执行写入,用
SELECT ... FOR UPDATE SKIP LOCKED防并发冲突,version乐观锁防两人同时审批。彻底解决"未审批先上线"+"后写覆盖" 两个高危问题。
6.3 sl_audit_log --- 审计日志表
治理表 · 变更前后快照,可回滚
设计思路:对 sl_metric、sl_metric_caliber、sl_synonym 三张核心表配置 AFTER INSERT/UPDATE/DELETE 触发器,自动将变更前后快照写入审计日志。支持任意版本回滚。
具体例子:回滚 pay_gmv 口径到上一版本
-- 步骤1:查历史快照
SELECT log_id, old_value, new_value, created_at, change_note
FROM sl_audit_log
WHERE table_name='sl_metric' AND record_id='pay_gmv'
ORDER BY created_at DESC LIMIT 5;
-- 查到 log_id=123, old_value 里有 preferred_table='dws_order_day'
-- 步骤2:从 old_value 恢复字段
UPDATE sl_metric SET
preferred_table = JSON_UNQUOTE(JSON_EXTRACT(
(SELECT old_value FROM sl_audit_log WHERE log_id=123), '$.preferred_table')),
version = version + 1,
updated_by = CURRENT_USER()
WHERE metric_id = 'pay_gmv';
合理性 :old_value/new_value 用 JSON 存完整快照,不需要额外建历史表。回滚操作就是从 old_value 提取字段值 UPDATE 回去,version+1。可回溯任一版本的任一字段变更,满足数据治理审计要求。
7. 配置拆分映射:一条 YAML 如何拆到多表
YAML 中的「一条指标配置」并非存为单行 JSON,而是按职责拆分到多张表。以 pay_gmv 为例展示完整映射:

拆分对照表
| YAML 配置项 | 存入表 | 关系 | 拆分理由 |
|---|---|---|---|
| name_cn, domain, type, aggregation | sl_metric | 1:1 | 主表基础属性 |
| preferred_table, physical_expr | sl_metric | 1:1 | 物理映射(首选路径) |
| fallback_table/where/expr | sl_metric | 1:1 | 回退路径 |
| formula, components | sl_metric | 1:1 | 派生公式 |
| synonyms | sl_synonym | 1:N | 多人无锁补充 |
| calibers | sl_metric_caliber | 1:N | primary + deprecated |
| available_dims | sl_metric_dim_avail | N:M | 双向查询 |
| join_graph | sl_join_path | 共享 | 以表为粒度复用 |
| global_filters | sl_global_filter | 共享 | 改一处全生效 |
| ---(派生) | sl_vector_doc | 1:1 | 事件异步生成 |
设计要点 :编译器通过
metric_id反向 JOIN 这 6 张表,重新组装出完整定义(5 分钟缓存)。拆分的好处是 职责单一、避免锁冲突、支持多人并行维护。如果存成单行 JSON,一人修改同义词会锁住整行,其他人无法同时修改口径。
8. 设计原则总结
语义层 12 张表的设计遵循以下核心原则:
| # | 原则 | 说明 | 体现表 |
|---|---|---|---|
| 1 | 职责分离 | 每张表只管一件事:sl_metric 管物理映射、sl_synonym 管同义词、sl_metric_caliber 管口径 | 全部 |
| 2 | 共享配置独立 | sl_join_path 和 sl_global_filter 跨指标共享,独立成表,修改一处全生效 | sl_join_path, sl_global_filter |
| 3 | 多对多用关联表 | 指标×维度是 M:N,用 sl_metric_dim_avail 而非 JSON 数组,便于双向查询和权限控制 | sl_metric_dim_avail |
| 4 | 派生数据解耦 | sl_vector_doc 是派生数据,source_id 逻辑关联而非物理外键,允许异步生成和短暂不一致 | sl_vector_doc |
| 5 | 审批状态机 | sl_change_request 保证口径变更必须审批,apply_worker 独立进程写入,防"未审批先上线" | sl_change_request |
| 6 | 审计可回滚 | sl_audit_log 存 old_value/new_value JSON 快照,从快照恢复字段即可回滚到任意版本 | sl_audit_log |
| 7 | 域隔离权限 | sl_user_role 的 domain_scope 限制 editor 只改自己域,SQL 层自动加 WHERE 过滤 | sl_user_role |
| 8 | 物理隔离 | 元数据库 chatbi_meta 与业务数仓物理隔离,sl_ 前缀统一命名,避免误操作影响业务数据 | 全部 |
核心设计哲学
语义层的本质是 结构化配置 + 确定性编译引擎。LLM 只负责意图理解(输出 DSL),SQL 编译全部由语义层引擎确定性处理。表结构设计围绕"多人协作维护 + 审批审计 + 版本回滚"展开,这是 YAML 文件方案无法实现的核心能力。
┌─────────────────────────────────────────────────────────────┐
│ ChatBI 语义层设计哲学 │
├─────────────────────────────────────────────────────────────┤
│ │
│ LLM(意图理解) ←──分离──→ 语义层引擎(SQL 编译) │
│ │ │ │
│ 输出 DSL JSON 查表 + 确定性编译 │
│ 不碰 SQL Join/口径/字段映射全确定 │
│ │
│ 表结构设计围绕三大能力: │
│ ┌─────────────┬─────────────┬─────────────┐ │
│ │ 多人协作维护 │ 审批审计 │ 版本回滚 │ │
│ │ │ │ │ │
│ │ • 角色权限 │ • 状态机 │ • 触发器快照 │ │
│ │ • 域隔离 │ • 乐观锁 │ • JSON 快照 │ │
│ │ • 行级锁 │ • 独立worker │ • version+1 │ │
│ └─────────────┴─────────────┴─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
附:12 张表速查表
| # | 表名 | 类型 | 核心职责 | 关键字段 |
|---|---|---|---|---|
| 1 | sl_dimension | 核心 | 维度主表(8项) | dim_id, physical_column, join_table |
| 2 | sl_dimension_hierarchy | 核心 | 维度层次(下钻/上卷) | dim_id, hierarchy, expr_sql |
| 3 | sl_metric | 核心 | 指标主表(47+项) | metric_id, preferred_table, fallback_* |
| 4 | sl_synonym | 核心 | 同义词映射 | term, target_type, target_id |
| 5 | sl_metric_caliber | 口径 | 口径冲突仲裁 | metric_id, caliber_type, definition |
| 6 | sl_metric_dim_avail | 关联 | 指标可用维度白名单 | metric_id, dim_id, allowed |
| 7 | sl_join_path | 关联 | Join 路径图谱 | fact_table, dim_table, on_keys |
| 8 | sl_global_filter | 关联 | 全局口径过滤器 | name, sql_where, enabled |
| 9 | sl_vector_doc | 向量 | 向量文档索引 | source_type, source_id, embedding |
| 10 | sl_vector_doc_dead_letter | 向量 | 向量生成失败补偿 | source_id, status, retry_count |
| 11 | sl_user_role | 治理 | 用户角色权限 | user_id, role, domain_scope |
| 12 | sl_change_request | 治理 | 审批状态机 | entity_id, status, submitter_id |
| 13 | sl_audit_log | 治理 | 审计日志(可回滚) | table_name, old_value, new_value |
注:含 P0-R3 修复新增的 sl_change_request,实际为 13 张表(原始设计 10 张 + P0 修复 3 张)。