02-数据底座
Part 2 · 数据底座
Section titled “Part 2 · 数据底座”问数技能再好也是无源之水——数据底座质量决定上限。本章讲怎么建星型宽表 + 本体概念层。
2.1 为什么先要宽表(vs 直接挂 OLTP)
Section titled “2.1 为什么先要宽表(vs 直接挂 OLTP)”直接让 LLM 查 OLTP 库的核心问题:
- JOIN 复杂度爆炸:业务系统通常 50-200 张表,跨表 JOIN 5-10 张是常态;LLM 写 JOIN 错误率随表数指数上升;
- 业务语义离散:OLTP 字段服务于事务,命名/类型/枚举都从交易角度设计(如
STATUS=3表示”已网签”),LLM 看不懂; - 性能不可预期:OLTP 没有为分析查询建索引,一个看似简单的聚合 SQL 可能扫全表;
- 数据语义陷阱密布:软删除、状态机、多版本快照、币种汇率、时区——OLTP 里堆着,分析时都要绕开。
宽表方案的本质好处:
- 把 N 张 OLTP 表的 JOIN 一次性预算到 1 张事实宽表,LLM 看到的就是「项目 × 科目 → 各类指标」这种直观结构;
- 业务语义在 ETL 阶段已经规整(如
is_jian_an=0/1已经在宽表里); - 物化的 JOIN 关系让查询响应稳定在 < 1s(MySQL 几十万行 / ClickHouse 千万行级别)。
2.2 星型宽表的核心布局
Section titled “2.2 星型宽表的核心布局” ┌────────────────────┐ │ 事实宽表 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。
2.3 本体(ttl)作为概念层
Section titled “2.3 本体(ttl)作为概念层”宽表服务于查询,本体服务于概念表达。两者关系:
┌──────────────────────────────┐│ 本体(cost.ttl / sales.ttl) │ 概念:业态、科目、项目、城市│ 描述:实体类型 / 关联 / 约束 │└──────────────┬───────────────┘ │ dwh:table / dwh:grain / dwh:filter 锚点 ▼┌──────────────────────────────┐│ schema.yaml │ 字段:dimension / measure / join└──────────────┬───────────────┘ │ SQL 物理列 ▼┌──────────────────────────────┐│ 宽表 dws_X_indicator │└──────────────────────────────┘本体的作用:
- 跨团队对齐概念:DBA、ETL、分析师、业务方对”项目""科目""业态”的定义统一;
- 作为 schema 的源头:schema.yaml 里的 cube 与本体里的 Class 一一对应;
- 演进治理:业务新增概念先在本体讨论,再下沉到 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(12 个)
Section titled “维度 cube(12 个)”| Cube | 业务实体 | 粒度 | 备注 |
|---|---|---|---|
| Project | 项目 | 项目级(含主数据 + 科目模板 + 省市 + 最新分期规划) | 核心维度 |
| City | 地理 | 省 + 市双层 | 通用维度 |
| ProductType | 业态(核算对象) | (项目, 业态) 粒度 | 项目内的业态切分 |
| BzItem | 成本科目 | 科目项(含层级路径) | 核心维度 |
| BzItemTemplate | 科目模板 | 模板字典 | 跨项目科目模板差异 |
| BqName | 工程量清单 | 清单 | 含合同/结算/预转固 |
| TreeNode | 清单模板库树 | 4 层 UNION | 模板穿透 |
| TalentMachineType | 人材机分类 | 分类节点 + 明细叶子 | 价格不在这(在 fact) |
| TalentMachineTypeAttr | 人材机额外属性 | 键值对 | 规格 / 材质 |
| SpecialEngineerType | 专业工程类型 | 字典 | 土建 / 安装 / 装饰 |
| ProjectModuleStandard | 项目建造标准 | 项目 × 标准 × 业态 | 工程特征下钻入口 |
| BusinessUnit | 公司组织 | 集团 → 区域 → 末级公司 | 跨公司查询 |
事实 cube(7 个)
Section titled “事实 cube(7 个)”| 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 领域化清单:你的领域有哪些核心实体”下面是几个典型领域的速写示意,不是设计方案,只是说明思路。
售楼系统(房产销售)
Section titled “售楼系统(房产销售)”可能的维度 cube:
- Project(项目,含项目位置、定位、开盘节奏)
- Building(楼栋)
- Unit(户型 / 房源)
- Customer(客户)
- Channel(渠道 / 案场)
- Salesperson(置业顾问)
- City / District(城市 / 区域)
- ProductType(产品业态)
- Promotion(促销活动)
可能的事实 cube:
- SalesContract(合同:网签 / 备案 / 退房状态机)
- VisitLog(来访 / 来电)
- Reservation(认筹 / 认购)
- PricingHistory(价格表历史快照)
- CommissionPayment(佣金结算)
可能的指标:去化率、客户转化率、价格成交比、人效、案场转化漏斗。
数据语义陷阱示例(待你领域填):
- “未网签 vs 已网签 vs 已备案 vs 已退房”的状态机:默认查询是不是要排除退房?汇总 GMV 是按签约价还是备案价?
- 多手房与一手房口径差异;
- 跨期客户(A 期来访 B 期成交)的归属。
工程系统(项目工程管理)
Section titled “工程系统(项目工程管理)”可能的维度 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(招聘漏斗)
数据语义陷阱示例:
- 在职 / 离职 / 待入职状态切换的时间锚点;
- 部门重组后的历史归属;
- 薪资版本(基本工资 / 总包 / 含奖金)口径差异。
2.6 宽表设计避坑清单
Section titled “2.6 宽表设计避坑清单”cost-query 团队踩过的坑,跨领域都常见。
坑 1:单表过宽(> 80 列)
Section titled “坑 1:单表过宽(> 80 列)”宽表 ≠ 把所有可能用得到的字段都塞一张。一旦字段数突破 80,会出现:
- DDL 改动成本高(几十万行 ALTER TABLE 几分钟到几十分钟);
- 字段语义难以维护,重名冲突频发;
- ETL 任务时长不可控。
经验值:维度宽表 < 40 列、事实宽表 < 60 列为宜。超过就该考虑垂直拆分或抽出 detail 表。
坑 2:字段命名不规范
Section titled “坑 2:字段命名不规范”cost-query 项目铁律:
- 字段一律下划线命名(
is_jian_an、bz_item_name); - 去除
CreateGUID / CreatedName / CreatedTime / ModifiedGUID / ModifiedName / ModifiedTime / VersionNumber等系统字段——LLM 看到这些会产生干扰; - 业务语义命名优先,避免 OLTP 风格的
type1/type2/flag1/flag2;非要用编码命名(如df_amount_type1-4),必须在 schema 描述里明确语义。
坑 3:枚举值不归一
Section titled “坑 3:枚举值不归一”OLTP 库里同一个状态可能在不同表里用不同枚举(is_active='Y'/'N' vs status=1/0/-1)。ETL 进宽表前必须归一,否则 LLM 拿到混乱的枚举值会反复试探。
推荐做法:
- 数值枚举统一用
0/1/2/...,含义在 schema 描述里写清; - 字符串枚举统一用语义清晰的字面值(
"contract"/"settlement"/"final"),避免缩写歧义。
坑 4:时间字段不规范
Section titled “坑 4:时间字段不规范”时间字段是问数高频维度,命名/类型不规范会带来麻烦。
推荐做法:
- 时间字段一律
_time后缀(price_time、signed_time),不要_date/_at/_ts混用; - 类型统一
DATETIME或DATE,不要VARCHAR存日期; - 派生字段(
price_year/price_month/price_quarter)由编译器在 DSL 层生成(用{self}.price_time+ DATE_FORMAT),不在宽表里物化——后者会让 dim 表字段数翻倍。
坑 5:父子层级处理粗糙
Section titled “坑 5:父子层级处理粗糙”业务实体经常有树形结构(科目层级、组织层级、楼栋-单元层级)。常见做法两种:
- 路径展开:在维度宽表里加
full_path_name/hierarchy_code/level等字段; - 父子共存:在事实宽表里既有父级行又有末级行(如 cost-query 的
is_end_cost=0/1)。
坑:父子共存的事实表如果不限定 is_end_cost 直接 SUM,会父子重复累加。cost-query 用 defaultFilters: isEndCost=1 在编译器层兜底,但你领域如果有类似结构(如”工程结构父子层级”),必须在 schema 描述里显式说明 + 编译器层加 defaultFilter。
坑 6:物理化 vs JOIN 的取舍
Section titled “坑 6:物理化 vs JOIN 的取舍”业务字段(如 city_name)可以放在维度表(dim_city.city_name,查询时 JOIN)也可以物化到事实表(fact.city_name,直接读)。两种各有适用:
- 频繁查询 + 业务字段稳定 → 物化到事实表(cost-query 2026-05 把 cityName/provinceName 物化后 JOIN 数量减半,查询延迟降 30%);
- 字典快速变化或字段多 → 保留 JOIN。
经验值:业务高频读、字段值不变(同一个城市不会突然改名)的,物化进事实表。
2.7 数据更新节奏
Section titled “2.7 数据更新节奏”问数技能对数据新鲜度和一致性有要求:
- 新鲜度:用户问”最近一个月的钢筋价格”时,宽表的数据应该至少更新到昨天。如果只更新到上周,得在 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 技能(不在本指南范围,但你的项目同样需要类似机制)。
2.8 接下来读什么
Section titled “2.8 接下来读什么”- 数据底座基本就绪 → 读 Part 3 Schema 双层架构;
- 数据底座还没开始 → 至少投入 1-3 个月做数据治理再继续;
- 想看 cost-query 完整宽表清单 → 读 ontology-model 仓 成本数据库宽表-表字段详细信息。
Part 2 完。读完应能回答:“为什么要宽表、星型布局长什么样、本体有什么用、我们领域的核心实体大致有哪些、宽表设计的 6 个常见坑”。