Skip to content

02-数据底座

问数技能再好也是无源之水——数据底座质量决定上限。本章讲怎么建星型宽表 + 本体概念层。


2.1 为什么先要宽表(vs 直接挂 OLTP)

Section titled “2.1 为什么先要宽表(vs 直接挂 OLTP)”

直接让 LLM 查 OLTP 库的核心问题

  1. JOIN 复杂度爆炸:业务系统通常 50-200 张表,跨表 JOIN 5-10 张是常态;LLM 写 JOIN 错误率随表数指数上升;
  2. 业务语义离散:OLTP 字段服务于事务,命名/类型/枚举都从交易角度设计(如 STATUS=3 表示”已网签”),LLM 看不懂;
  3. 性能不可预期:OLTP 没有为分析查询建索引,一个看似简单的聚合 SQL 可能扫全表;
  4. 数据语义陷阱密布:软删除、状态机、多版本快照、币种汇率、时区——OLTP 里堆着,分析时都要绕开。

宽表方案的本质好处

  • 把 N 张 OLTP 表的 JOIN 一次性预算到 1 张事实宽表,LLM 看到的就是「项目 × 科目 → 各类指标」这种直观结构;
  • 业务语义在 ETL 阶段已经规整(如 is_jian_an=0/1 已经在宽表里);
  • 物化的 JOIN 关系让查询响应稳定在 < 1s(MySQL 几十万行 / ClickHouse 千万行级别)。

┌────────────────────┐
│ 事实宽表 1(粒度 A)│
│ dws_X_indicator │ ← 业务指标已预聚合
└─────────┬──────────┘
│ 维度外键
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ dim_X │ │ dim_Y │ │ dim_Z │ ← 维度宽表(已物化层级、属性)
└──────────┘ └──────────┘ └──────────┘
▲ ▲ ▲
│ │ │
┌─────────────────────────────────────────┐
│ 事实宽表 2(粒度 B,明细级) │
│ dwd_X_detail │ ← 穿透下钻入口
└─────────────────────────────────────────┘

分层约定

前缀含义
dim_维度宽表dim_project / dim_city / dim_customer
dwd_事实宽表-明细级(DataWarehouse Detail)dwd_unit_price(清单明细价格)
dws_事实宽表-汇总级(DataWarehouse Summary)dws_project_indicator(项目指标汇总)

为什么分 dwd / dws 两层

  • dwd 保留最细粒度(每条明细一行),用于穿透查询(“这个项目里钢筋的具体单价明细”);
  • dws 是 dwd 的预聚合(如按”项目 × 科目”维度 SUM),用于汇总查询(“这个项目的钢筋单方造价”),响应快得多。

cost-query 实例:

  • dws_cost_project_indicator:项目 × 科目 × 业态粒度的指标汇总(建面单方/单位造价/含量/综合单价);
  • dwd_cost_bq_unit_price:清单明细级单价;
  • 当用户问”项目的钢筋单方”走 dws,问”钢筋的具体清单明细”走 dwd。

宽表服务于查询,本体服务于概念表达。两者关系:

┌──────────────────────────────┐
│ 本体(cost.ttl / sales.ttl) │ 概念:业态、科目、项目、城市
│ 描述:实体类型 / 关联 / 约束 │
└──────────────┬───────────────┘
│ dwh:table / dwh:grain / dwh:filter 锚点
┌──────────────────────────────┐
│ schema.yaml │ 字段:dimension / measure / join
└──────────────┬───────────────┘
│ SQL 物理列
┌──────────────────────────────┐
│ 宽表 dws_X_indicator │
└──────────────────────────────┘

本体的作用

  1. 跨团队对齐概念:DBA、ETL、分析师、业务方对”项目""科目""业态”的定义统一;
  2. 作为 schema 的源头:schema.yaml 里的 cube 与本体里的 Class 一一对应;
  3. 演进治理:业务新增概念先在本体讨论,再下沉到 schema 与宽表,避免 schema 字段名风格不一致。

本体不是必须

  • 团队规模小(< 5 人)且业务概念稳定时,可以省掉本体直接在 schema.yaml 里写;
  • 团队规模大、概念跨多个系统共享时,本体是必要的。

cost-query 走的是「本体作为概念锚点」路线——cost.ttl 只保留 dwh:table / dwh:grain / dwh:filter 三个高层注解,字段级映射全部下沉到 schema.yaml。这种轻量本体方案推荐给大多数团队。


2.4 cost-query 实例:维度 12 / 事实 7 的取舍

Section titled “2.4 cost-query 实例:维度 12 / 事实 7 的取舍”

来看一个完整领域的 cube 清单是怎么取舍的。

Cube业务实体粒度备注
Project项目项目级(含主数据 + 科目模板 + 省市 + 最新分期规划)核心维度
City地理省 + 市双层通用维度
ProductType业态(核算对象)(项目, 业态) 粒度项目内的业态切分
BzItem成本科目科目项(含层级路径)核心维度
BzItemTemplate科目模板模板字典跨项目科目模板差异
BqName工程量清单清单含合同/结算/预转固
TreeNode清单模板库树4 层 UNION模板穿透
TalentMachineType人材机分类分类节点 + 明细叶子价格不在这(在 fact)
TalentMachineTypeAttr人材机额外属性键值对规格 / 材质
SpecialEngineerType专业工程类型字典土建 / 安装 / 装饰
ProjectModuleStandard项目建造标准项目 × 标准 × 业态工程特征下钻入口
BusinessUnit公司组织集团 → 区域 → 末级公司跨公司查询
Cube粒度用途
ProjectIndicator项目 × 科目(含父级+末级)× (业态)项目级指标汇总
ProjectIndicatorDetail项目 × 清单 × 明细条目项目指标穿透
BqUnitPrice清单 × 明细条目清单明细工程量 + 单价
TalentMachinePrice每条价格记录人材机价格库
BqIndicator清单 × 查询维度(清单/业态/专业级)含税金额、建设规模
BqBzItemIndicator清单 × 科目 × element_type清单科目指标
BqUnitTalentMachineDetail单位工程 × 分部分项 × 清单项 × 定额 × 人材机编码最细粒度的人材机消耗

为什么有 BzItem 还有 BzItemTemplate:跨项目的同名科目(如”钢筋”)编码可能不同(房开模板用 A.03.02.01.02,东航模板用 B.05.01)——所以 BzItem 是项目特定的,BzItemTemplate 是跨项目共享的字典。

为什么 TalentMachineType(分类字典)不在事实 cube 里带价格:分类字典是「钢筋/混凝土/水泥」这种节点,节点本身没有价格;价格是「螺纹钢 HRB400 在 2026-01 的成交价」这种实例。两者粒度不同,强行合并会让维度字段语义混乱。

为什么有 ProjectIndicator(汇总)还有 ProjectIndicatorDetail(明细):前者按项目 × 科目预聚合,回答”项目的钢筋单方造价”快;后者保留每条清单明细,回答”项目的钢筋具体由哪些清单条目组成”。一汇总一穿透,对应不同查询深度。


2.5 领域化清单:你的领域有哪些核心实体

Section titled “2.5 领域化清单:你的领域有哪些核心实体”

下面是几个典型领域的速写示意,不是设计方案,只是说明思路。

可能的维度 cube:

  • Project(项目,含项目位置、定位、开盘节奏)
  • Building(楼栋)
  • Unit(户型 / 房源)
  • Customer(客户)
  • Channel(渠道 / 案场)
  • Salesperson(置业顾问)
  • City / District(城市 / 区域)
  • ProductType(产品业态)
  • Promotion(促销活动)

可能的事实 cube:

  • SalesContract(合同:网签 / 备案 / 退房状态机)
  • VisitLog(来访 / 来电)
  • Reservation(认筹 / 认购)
  • PricingHistory(价格表历史快照)
  • CommissionPayment(佣金结算)

可能的指标:去化率、客户转化率、价格成交比、人效、案场转化漏斗。

数据语义陷阱示例(待你领域填):

  • “未网签 vs 已网签 vs 已备案 vs 已退房”的状态机:默认查询是不是要排除退房?汇总 GMV 是按签约价还是备案价?
  • 多手房与一手房口径差异;
  • 跨期客户(A 期来访 B 期成交)的归属。

可能的维度 cube:

  • Project(项目)
  • Building / WorkSection(楼栋 / 标段)
  • Contractor(承建方)
  • Specialty(专业:土建 / 钢结构 / 机电)
  • Material(材料)
  • Stage(工程阶段:基础 / 主体 / 装修 / 收尾)

可能的事实 cube:

  • ConstructionProgress(进度:计划 vs 实际)
  • MaterialConsumption(材料消耗)
  • LaborAttendance(劳务出勤)
  • QualityInspection(质量验收)
  • SafetyIncident(安全事件)
  • ChangeOrder(变更签证)

可能的指标:进度偏差、成本偏差、质量合格率、安全事件率、变更频次。

数据语义陷阱示例:

  • “计划 vs 实际 vs 验收”三态,默认查询是哪一态?
  • 单位工程 / 分部 / 分项 / 工序的层级混算(与 cost-query 的父子科目混算同构);
  • 暂估价 / 合同价 / 结算价的口径切换。

可能的维度 cube:

  • Employee(员工,含层级 / 司龄 / 技术栈)
  • Department(部门)
  • Position(岗位)
  • City / Office(办公地)

可能的事实 cube:

  • AttendanceRecord(考勤)
  • LeaveRequest(请假)
  • PerformanceReview(绩效评估)
  • CompensationHistory(薪资历史)
  • RecruitmentFunnel(招聘漏斗)

数据语义陷阱示例:

  • 在职 / 离职 / 待入职状态切换的时间锚点;
  • 部门重组后的历史归属;
  • 薪资版本(基本工资 / 总包 / 含奖金)口径差异。

cost-query 团队踩过的坑,跨领域都常见。

宽表 ≠ 把所有可能用得到的字段都塞一张。一旦字段数突破 80,会出现:

  • DDL 改动成本高(几十万行 ALTER TABLE 几分钟到几十分钟);
  • 字段语义难以维护,重名冲突频发;
  • ETL 任务时长不可控。

经验值:维度宽表 < 40 列、事实宽表 < 60 列为宜。超过就该考虑垂直拆分或抽出 detail 表。

cost-query 项目铁律:

  • 字段一律下划线命名(is_jian_anbz_item_name);
  • 去除 CreateGUID / CreatedName / CreatedTime / ModifiedGUID / ModifiedName / ModifiedTime / VersionNumber 等系统字段——LLM 看到这些会产生干扰;
  • 业务语义命名优先,避免 OLTP 风格的 type1 / type2 / flag1 / flag2;非要用编码命名(如 df_amount_type1-4),必须在 schema 描述里明确语义。

OLTP 库里同一个状态可能在不同表里用不同枚举(is_active='Y'/'N' vs status=1/0/-1)。ETL 进宽表前必须归一,否则 LLM 拿到混乱的枚举值会反复试探。

推荐做法

  • 数值枚举统一用 0/1/2/...,含义在 schema 描述里写清;
  • 字符串枚举统一用语义清晰的字面值("contract" / "settlement" / "final"),避免缩写歧义。

时间字段是问数高频维度,命名/类型不规范会带来麻烦。

推荐做法

  • 时间字段一律 _time 后缀(price_timesigned_time),不要 _date/_at/_ts 混用;
  • 类型统一 DATETIMEDATE,不要 VARCHAR 存日期;
  • 派生字段(price_year / price_month / price_quarter)由编译器在 DSL 层生成(用 {self}.price_time + DATE_FORMAT),在宽表里物化——后者会让 dim 表字段数翻倍。

业务实体经常有树形结构(科目层级、组织层级、楼栋-单元层级)。常见做法两种:

  • 路径展开:在维度宽表里加 full_path_name / hierarchy_code / level 等字段;
  • 父子共存:在事实宽表里既有父级行又有末级行(如 cost-query 的 is_end_cost=0/1)。

:父子共存的事实表如果不限定 is_end_cost 直接 SUM,会父子重复累加。cost-query 用 defaultFilters: isEndCost=1 在编译器层兜底,但你领域如果有类似结构(如”工程结构父子层级”),必须在 schema 描述里显式说明 + 编译器层加 defaultFilter。

业务字段(如 city_name)可以放在维度表(dim_city.city_name,查询时 JOIN)也可以物化到事实表(fact.city_name,直接读)。两种各有适用:

  • 频繁查询 + 业务字段稳定 → 物化到事实表(cost-query 2026-05 把 cityName/provinceName 物化后 JOIN 数量减半,查询延迟降 30%);
  • 字典快速变化或字段多 → 保留 JOIN。

经验值:业务高频读、字段值不变(同一个城市不会突然改名)的,物化进事实表。


问数技能对数据新鲜度一致性有要求:

  • 新鲜度:用户问”最近一个月的钢筋价格”时,宽表的数据应该至少更新到昨天。如果只更新到上周,得在 SKILL.md 里告知用户当前数据时效;
  • 一致性:宽表更新过程中不能让用户查到一半新一半旧的数据。cost-query 用「临时表 + 原子 RENAME」策略:先把新数据写到 dws_X_indicator_tmp,写完后 RENAME TABLE dws_X_indicator → dws_X_indicator_old, dws_X_indicator_tmp → dws_X_indicator,毫秒级切换;
  • 回滚能力:每次重洗保留前一版的 _old 表 24 小时,发现问题能立即回滚。

详细策略见 ontology-model 仓 cost-clean 技能(不在本指南范围,但你的项目同样需要类似机制)。


Part 2 完。读完应能回答:“为什么要宽表、星型布局长什么样、本体有什么用、我们领域的核心实体大致有哪些、宽表设计的 6 个常见坑”。