# 给数据上户口:企业数据查询助手的底座工程
> 老板问”A项目上个月营收多少、花了多少、进度怎么样”,你从财务、开支、进度三张表取数,结果营收对得上、花费对不上、进度直接为空。这不是 SQL 写错了,而是三张表说的不是同一种”语言”。本篇是「企业数据查询助手」系列第一篇,解决最底层的痛点:如何用「双ID联合主键 + 主档表/事实表分层」方案,让所有业务表共享同一套主键语言,给后续接入大模型铺好地基。
—
**一、问题诊断:跨表关联为什么会失真**
先看一段”理想中应该能跑通”的查询:
“`sql
— 这段在做:用项目编号关联三张表,一次取全
SELECT f.revenue, e.expense, p.completion
FROM finance_fact f
JOIN expense_fact e ON f.proj_code = e.proj_code
JOIN progress_fact p ON f.proj_code = p.proj_code
WHERE f.proj_code = ‘A001′
“`
执行结果:营收对、花费错位、进度为空。
**根因不在 SQL,在主键。** 三张表的 `proj_code` 来自三个独立系统:财务系统按合同编号编、项目系统按立项编号编、进度系统按交付批次编。同一个”A001″在每张表里指向不同实体,关联等于”同名异指”。
这种现象在数据工程里叫**编码孤岛**——每张表都有自己的身份证系统,彼此不互通。要解决,必须先做一件事:停止建表,先定主键。
—
**二、主键设计:为什么必须用双ID联合主键**
我们定的强制规则是:**所有业务表,必须同时包含 `项目ID` 和 `产品ID` 两个字段,缺一不可。** 这不是冗余,是关联的唯一钥匙。
【为什么单ID不够】
– 只用项目ID:一个项目可同时推进多条产品线(同一智慧园区含安防、能源、停车三个产品)。单ID关联会把三条产品线的营收、成本全部混在一起,无法拆分到产品粒度。
– 只用产品ID:同一产品可部署在多个项目上。单ID无法区分这笔营收来自 A 项目还是 B 项目。
**双ID的语义边界:项目ID管”在哪发生”,产品ID管”关于什么”。两者合一,才能保证数据不串门。**
【联合主键的物理实现】
主档表用 `(项目ID, 产品ID)` 作联合主键;所有事实表复制这两个字段并建联合索引。这样任何一张事实表都能直接和主档表对齐,不需要中间映射层。
但这里有一个前提:**业务系统原始ID必须先全局统一**(见第五节)。否则双ID本身也会变成新的孤岛。
▸ 小贴士:联合主键会让索引体积膨胀,但查询收益远大于存储成本。在千万级数据量下,联合索引比单索引 + 应用层拼接快一个数量级。
—
**三、分层表结构:主档表 + 事实表**
主键定好后,表结构用数仓最经典的星型模型分两层。
【第一层:主档表(dimension,唯一一张)】
存最稳定的静态属性,是所有查询的统一入口:
– `项目ID`、`产品ID`(联合主键)
– 项目名称、产品名称
– 项目状态(枚举:进行中/已结束/暂停)
– 所属区域、负责人
主档表行数少、变更频率低,适合被所有事实表反复 JOIN。
【第二层:事实表(fact,按业务域拆分多张)】
每张管一个业务维度,全部带双ID,用于关联主档表。按域拆分而非按时间拆分,是为了让每个业务域独立维护、独立扩容。
每张事实表额外包含 `账期` 字段(格式 `YYYY-MM`)。这是时间维度筛选的标准入口——没有账期,跨月聚合就会退化为全表扫描。
**分层的关键收益:静态属性集中存一次,动态数据按域独立增长,互不污染。**
—
**四、关联查询范式:标准模板与 JOIN 选择**
数据模型搭好后,查询逻辑收敛为一个固定范式:
“`sql
— 这段在做:用双ID关联主档表与三张事实表
SELECT p.项目名称, p.产品名称,
f.revenue, e.expense, g.completion_pct
FROM master_dim p
JOIN finance_fact f ON p.项目ID=f.项目ID AND p.产品ID=f.产品ID
JOIN expense_fact e ON p.项目ID=e.项目ID AND p.产品ID=e.产品ID
JOIN progress_fact g ON p.项目ID=g.项目ID AND p.产品ID=g.产品ID
WHERE p.项目名称=’智慧园区’
AND f.账期=’2026-06′ AND e.账期=’2026-06′
“`
所有查询遵循同一模式:主档表定位 → 双ID下推 → 各事实表对齐取数。
【为什么默认 INNER JOIN 而非 LEFT JOIN】
INNER JOIN 保证只返回三张事实表都有数据的组合,避免”看似有结果实则半空”的假阳性。若某产品当月确实无进度数据,应让它显式缺失,而不是用 NULL 填充造成误读。**LEFT JOIN 是业务确认后的例外,不是默认。**
—
**五、前置条件与边界**
这套方案有两个硬前提,不满足需先补齐:
【条件1:ID全局唯一性】
业务系统各自生成ID时,先建 ID 映射表,把不同系统的编码对应关系维护好:
“`sql
— 这段在做:建ID映射表,对齐多系统编码
CREATE TABLE id_mapping (
internal_proj_id VARCHAR(20) PRIMARY KEY,
finance_code VARCHAR(20),
project_code VARCHAR(20)
);
“`
映射表是过渡工程,最终目标是用一套统一编码替代所有系统原始ID。
【条件2:数据量与分区策略】
数据量超过千万级时,事实表按 `账期` 做月分区。每次查询强制带账期范围,让分区裁剪生效,避免全表扫描。
▸ 小贴士:历史数据已混乱时,不要一次性全量迁移。选当前年度先做试点,验证双ID关联链路无误,再逐步回刷历史。
—
**写在最后**
本篇只做一件事:**给数据上户口。** 让每一张表都用同一套双ID主键说话,是搭建数据查询助手的地基工程。
地基不牢,后面接入大模型只会放大错误——AI 再聪明,喂进去的数据是错的,输出的结论必然是错的。**底层不准,上层全废。**
**如果你正要开始搭建**,本周末可做一件事:梳理手上所有业务表,逐张检查是否有统一的项目/产品标识符。没有就列一张”ID映射清单”——这是你下周开工的第一份待办。
下一篇我们往上走一层:数据整齐之后,如何用语义映射层让大模型准确理解业务查询意图。
> 你在公司里遇到过”同一项目在不同系统编号不同”的坑吗?欢迎评论区聊聊你的解法。