别再让大模型瞎写 SQL:维度建模 + 指标体系,落地真正的指标智能问数

0 阅读14分钟

别再让大模型瞎写 SQL:维度建模 + 指标体系,落地真正的指标智能问数

一句话总结:让大模型"不写 SQL",只负责把自然语言翻译成指标、维度、业务限定、时间范围四要素,剩下的交给语义层确定性编译成 SQL。口径不跑偏,数据可信,2 核 4G 就能跑。


一、Text2SQL 的天花板,不在模型,在语义

过去一年,"自然语言问数"几乎成了每个数据平台的标配。做法也很直接:把建表语句(DDL)塞给大模型,让它生成一段 SQL,执行后返回结果。

Demo 很惊艳,一上生产就翻车:

  • 口径对不上:你问"销售额",模型 SUM(amount);财务的销售额要扣退款、只算已支付。同一个词,两个数。
  • 幻觉表名字段名:表一多、字段一杂,fact_order 和 fact_sales_order 傻傻分不清,生成的 SQL 直接报表不存在。
  • 中文业务词映射难:"北京"到底是 dim_store.city 还是 dim_region.province?"最近一周"从哪天算起?
  • 权限失控:模型生成的 SQL 绕过了数据权限,用户看到了不该看的数据。
  • 不可解释:出了问题,没人知道这段 SQL 是怎么来的。

问题的根子在于:我们让一个概率模型,去承担本该由确定性系统承担的口径与权限职责。

正确的分工应该是:

自然语言  ──►  LLM:只做「语义解析」  ──►  四要素
(用户提问)        (理解意图)          (指标/维度/限定/时间)
                                              │
                                              ▼
                                    语义层:确定性编译为 SQL
                                    (口径、权限、方言全在这里)
                                              │
                                              ▼
                                            数据结果

而"语义层",在数据仓库里有一个成熟的名字——维度建模 + 指标体系。


二、概念篇:维度建模与指标管理

2.1 维度建模:给数据一个"业务坐标"

维度建模(Dimensional Modeling)是 Ralph Kimball 提出的一套面向分析的数据组织方法,核心就两个概念:

概念含义例子
事实表(Fact)业务过程中发生的可度量事件一笔销售订单、一次访问
维度表(Dimension)描述事件的业务观察角度谁(客户)、什么(商品)、哪里(门店)、何时(日期)

事实表保存度量值与指向各维度的外键,维度表保存描述性属性。以电商为例:

                    ┌─────────────┐
                    │  dim_product│
                    │  商品维度    │
                    └──────┬──────┘
                           │ product_sk
┌─────────────┐     ┌──────┴──────────┐     ┌─────────────┐
│  dim_store  │────▶│ fact_sales_order│◀────│ dim_customer│
│  门店维度    │store│     事实表       │cust │  客户维度    │
└─────────────┘  _sk│  amount         │ _sk └─────────────┘
                    │  quantity       │
                    │  cost_amount    │
                    └──────┬──────────┘
                           │ order_date_sk
                    ┌──────┴──────┐
                    │  dim_date   │
                    │  日期维度    │
                    └─────────────┘

这种"事实表居中、维度表环绕"的结构叫星型模型;如果维度再拆出子维度(如商品→品牌→品类),就是雪花模型。

维度建模的价值,是给冷冰冰的表和字段,建立起一张业务语义地图:"amount 是金额,它挂在订单上,可以从门店、商品、时间等任意角度看"。这正是大模型最缺、也最需要的东西。

在工程落地时,维度通常还会细分三类,用来覆盖常见的分析诉求:

维度类型说明典型场景
普通维度平铺的成员列表客户、门店、商品
层级维度多级从属,如 一级品类 → 二级 → 三级品类、组织架构、行政区划
时间维度年 / 季 / 月 / 周 / 日多粒度趋势、同环比、周期对比

2.2 指标管理:把"口径"沉淀成资产

如果说维度建模定义了"从哪些角度看数据",那指标管理定义的就是"看什么、怎么算"。

指标通常分两类:

  • 原子指标:建立在某个事实表字段上的聚合,是口径的最小单元。 例如 销售额 = SUM(amount)、订单数 = COUNT(DISTINCT order_id)。
  • 衍生指标:由原子指标(或其他衍生指标)通过公式组合而来。 例如 毛利率 = 毛利 / 销售额、客单价 = 销售额 / 订单数。

一个完整的指标定义,除了名字和公式,还要包含:

  • 业务限定(Filter):只统计已支付订单、只看直营门店等;
  • 单位与数据类型:元、人、%、DECIMAL;
  • 可加性:能否跨维度求和(如"订单数"不可加);
  • 所属目录:让成百上千个指标有组织、可检索。

指标体系 = 维度建模 + 指标管理。前者提供"分析坐标",后者提供"统一口径"。二者合在一起,就构成了智能问数真正依赖的语义层。


三、基于 DataY 构建维度模型与指标体系

DataY 是一个"极致精简"的轻量数据平台:一个 JAR、内嵌 DuckDB,2 核 4G 就能跑通「数据集成 → 维度建模 → 任务调度 → 高性能数仓 → 智能问数」全链路。下面用平台预置的电商示例,走一遍建模到问数的闭环。

登录后,示例资产已开箱即用:

首页资产概览

3.1 三种维度类型,字段自动生成

在「数据模型 → 维度建模」里新建维度模型时,选对类型能省掉大量手写工作:

  • 普通维度:自动带出 member_id / member_name 成员字段;
  • 层级维度:按层级数自动生成 level1_id / level1_name … levelN_id / levelN_name;
  • 时间维度:按勾选的粒度自动生成 year_id / quarter_id / month_id / day_id、主键 date_key 以及层级路径 hierarchy。

时间维度是最高频的。勾选「年 / 季 / 月 / 日」后,平台会按由粗到细自动排序,并生成对应的 id/name 字段:

时间维度粒度勾选

保存后点「物化」,一次性把维度表建到内嵌 DuckDB,并勾选"生成预置数据",平台会按日期范围逐天生成日期数据(如 2024-01-15 → year_id=2024、quarter_id=2024Q1、month_id=202401、day_id=20240115):

物化弹窗

3.2 事实表关联维度:语义的源头

事实表的建模同样简单:以"注册模式"从物理表导入字段,然后给每个外键字段在「关联维度」列指向对应维度模型,再指定一个「时间周期字段」。

以 fact_sales_order_item 为例,order_date_sk → dim_date、product_sk → dim_product、store_sk → dim_store:

事实表字段关联维度配置

这一步是整个方案的关键:关联关系一旦建立,指标就自动获得了业务语义——它能从哪些维度看、用哪个字段做时间过滤,全部由事实表的外键与时间字段推导而来,不需要在指标里重复声明。后续新增维度,指标也自动可用。

3.3 原子指标:一个聚合,一个口径

在「指标管理」中新建原子指标:绑定事实表、写好聚合公式、配置业务限定即可。

新建原子指标表单

例如:

指标公式说明
销售额 sales_amountSUM(amount)单位:元
销售量 sales_quantitySUM(quantity)件
毛利 gross_profitSUM(profit)元
订单数 order_countCOUNT(DISTINCT order_id)不可加

保存前可以「预览 SQL」核对口径,避免"建完才发现算错"。

3.4 衍生指标:用 ${编码} 组合口径

衍生指标不绑定事实表,而是用 ${指标编码} 引用其他指标,纯算术组合:

毛利率  gross_profit_rate = ${gross_profit} / ${sales_amount}
客单价  avg_order_value   = ${sales_amount} / ${order_count}

衍生指标公式编辑

平台会对衍生公式做严格校验:只能引用已存在的指标、只能包含算术运算、并做环路检测(防止 A 引用 B、B 又引用 A)。被引用的指标不允许删除,保证口径链完整。

到这里,一套"事实表 + 三维度 + 指标体系"的语义层就搭好了。它不依赖大模型,本身就能通过「公共指标查询面板」手动选指标、选维度、设时间,直接出数——这也成了校验 AI 口径的基准工具:

公共指标查询面板


四、语义检索增强:默认 DuckDB 最简向量库,可扩展其他向量库

语义层建好了,但大模型还是听不懂"销售额""北京""最近一周"这些口语。DataY 的做法是:把指标、维度、维度成员向量化,问数前先做一轮语义检索(RAG),再把候选喂给模型。

而向量存储与检索,默认走一条最简路径:直接用内嵌 DuckDB 承载,不引入额外组件;同时把检索层设计成可替换的,业务长大后能平滑接入专业向量库。

4.1 为什么要 RAG

指标动辄成百上千,如果全量塞进 Prompt,token 爆炸且模型容易分心。RAG 的作用是先召回、再精排:

  • 用户说"销售额",但指标库里叫"销售金额"——靠向量相似度召回;
  • 用户说"北京",要能定位到 dim_store.city——靠维度成员的向量召回;
  • 用户说"各品类",要能对上层级维度 dim_category——靠维度字段的向量召回。

4.2 独立 DuckDB 文件 + 三张向量表

向量库是独立的 DuckDB 文件(默认 ./data/metric-rag.duckdb),与业务数仓物理隔离,互不影响。核心三张表:

表存什么
rag_metric指标:编码、名称、口径描述、向量
rag_dimension维度字段:维度编码、字段名、是否层级、向量
rag_member维度成员值:如"北京""华东""手机",用于口语值对齐

其中维度成员索引是最容易被忽略、却最见功力的一层。平台会自动采样可作为过滤值的字符串字段(排除主键、*_id、*_code、日期等),执行 SELECT DISTINCT 字段 ... LIMIT N 拿到真实取值并向量化。于是:

"北京" 的向量,能命中 dim_store.city 下的成员 "北京" —— 模型就知道该给哪个维度字段加 city = '北京'。

4.3 默认最简模式:DuckDB 核心函数,免装 vss

很多方案一提到 DuckDB 向量库,第一反应是 INSTALL vss、建 HNSW 索引。DataY 的默认选择是更简的一档:不装任何扩展,直接用 DuckDB 核心函数做余弦检索。

判断依据很朴素:指标、维度、维度成员的量级通常只有几百到几万条,暴力扫描足够快,没必要引入扩展依赖和额外的索引维护成本。

向量以 JSON 字符串存入 VARCHAR 字段,检索时直接调用 DuckDB 核心函数 list_cosine_similarity:

SELECT code, name,
       list_cosine_similarity(embedding::FLOAT[], ?::FLOAT[]) AS score
FROM rag_metric
WHERE tenant_id = ?
ORDER BY score DESC
LIMIT ?;

4.4 需要时再升级:检索层可扩展其他向量库

DuckDB 是默认最简模式,不是唯一模式。当指标规模增长到数十万级、成员值海量,或团队本身已有向量基础设施时,可以平滑接入 Milvus、pgvector、Elasticsearch、Chroma 等专业向量库:

  • 接口抽象:向量化已抽象为独立的 EmbeddingClient 接口,检索层也按可替换的方式设计,上层工具链(retrieve_metric_context 等)与问数逻辑不感知底层存储;
  • 按需切换:更换向量库只需替换实现与配置,指标/维度建模、指标管理、问数助手全部无需改动;
  • 渐进演进:小规模用 DuckDB 最简模式起步,规模上来再升级,不必一开始就为向量库付运维成本。

一句话:默认用最简方案把链路跑通,规模需要时再升级,而不是一开始就上重型组件。

4.5 两档 Embedding,默认零依赖

Embedding 提供两种实现,通过 datay.ai.rag.provider 切换:

provider实现特点
local(默认)字符 1–3 gram 哈希(FNV-1a)+ L2 归一化完全离线、零 Key、零外部依赖,对中文同形近义友好
openai兼容 OpenAI 的 /embeddings 接口默认 text-embedding-3-small,质量更高

这点对私有化部署特别重要:没网、没有 Embedding Key,也能开箱即用地跑智能问数。

另外有个小心思:指标做 embedding 时,名称会被重复拼接三次,用于加权。这样"销售额"不会被"平均销售价格"稀释掉,召回更准。


五、指标智能问数:让 LLM 只做翻译,SQL 交给系统

前面所有铺垫,都是为了这一刻。DataY 的「智能问数」设计有一条铁律:

大模型不写 SQL,只输出结构化的四要素;SQL 由服务端确定性编译。

5.1 四要素解析

用户问:"最近一周北京的销售额",模型要拆成:

要素解析结果
指标sales_amount
维度无(或按需)
业务限定dim_store.city = '北京'
时间范围start = 今天 − 6 天、end = 今天

再复杂一点:"上个月各品类的销售额和毛利率":

  • 指标:sales_amount、gross_profit_rate(衍生指标自动展开公式)
  • 维度:dim_category(层级维度,默认取最细层级)
  • 时间:timeRange = 上月首日 ~ 上月末日

而"今年每个月的销售趋势",模型不会逐月查 12 次,而是把时间维度 dim_date 的 year, month 作为分组维度,一次查询出趋势。

5.2 五个工具,串起完整链路

问数助手(metric-query)只挂载 5 个只读工具:

工具作用
retrieve_metric_context用用户原话做向量检索,召回候选指标 / 维度字段 / 维度成员(带相似度)
list_metrics拉取完整指标清单(可带关键字),做模糊匹配确认
describe_metrics返回所选指标可用的真实维度、层级、时间字段
sample_dimension_values采样某维度字段的真实取值,确认"北京"该落哪个字段
query_metric_data传入四要素,执行查询并返回数据

一次问数的执行流:

用户提问
   │
   ▼
① retrieve_metric_context  ← DuckDB 向量库语义召回
   │  "销售额"→sales_amount, "北京"→dim_store.city
   ▼
② list_metrics / describe_metrics  ← 用真实元数据核对
   │  拿到维度编码、层级序号、时间粒度字段
   ▼
③ query_metric_data  ← 传入结构化的四要素
   │
   ▼
服务端 MetricQueryService:确定性编译 SQL → 执行
   │
   ▼
Markdown 表格回答(不含 SQL)

智能问数页头

一次完整问数的解析与结果

5.3 服务端确定性编译 SQL

query_metric_data 拿到的不是 SQL,而是一份结构化参数(指标编码数组、维度数组、业务限定、时间范围)。服务端据此编译:

  • 单事实表:以事实表为主表,按维度外键 LEFT JOIN 维度表,WHERE 拼接业务限定与时间条件,GROUP BY 分组字段;
  • 多事实表:各事实表各生成子查询,再按共同维度做 FULL OUTER JOIN,用 COALESCE 合并维度展示值——所以"销售额和订单数一起看"这种跨事实指标也能一次取数;
  • 层级/时间维度:按层级序号取 levelN_id 分组、levelN_name 展示,天然支持"按一级品类""按月"这类粒度;
  • 衍生指标:沿 ${code} 引用递归展开成嵌套的聚合表达式,环路由前面的校验兜底。

因为 SQL 是编译出来的,口径 100% 来自指标定义,不存在模型编造表名、字段名或口径的可能。

5.4 权限:不是过滤,是前置

数据权限在这里不是"查完再过滤",而是编译进 SQL 的强制前置条件。角色配置的维度成员范围(如某区域经理只能看华东门店),会作为 WHERE 的第一段拼在最前面,用户提交的业务限定只能 AND 在后面,无法绕过。缺失维度时还可配置 SKIP(跳过并告警)或 DENY(直接拒绝)策略。

5.5 顺带一提:把语义层开放给任意 AI 客户端(MCP)

除了平台内置的问数助手,DataY 还把这 5 个工具封装成了 MCP Server(/mcp/metric)。这意味着:

Claude Desktop、Cursor 等任意支持 MCP 的客户端,用自己的大模型,就能直接查询你的指标体系——服务端不需要任何 LLM Key。

同一套语义层与权限控制,既能"内置问数",也能"对外开放"。


六、小结:这套架构为什么值得借鉴

回到开头的痛点,DataY 的答案其实就三句话:

  1. 确定性归系统,生成式归模型。让大模型做它擅长的语义理解,把口径、SQL、权限交给确定性的语义层,从根上消灭幻觉。
  2. 语义层是护城河。维度建模提供"分析坐标",指标管理沉淀"统一口径",再叠加一层向量检索做口语对齐——三者合起来,才是可用的智能问数。
  3. 能用简单方案解决的,绝不堆组件。向量库用 DuckDB 核心函数 + 本地哈希 Embedding,不装 vss、不依赖外部 Embedding 服务;整个平台一个 JAR、2 核 4G 起步。

智能问数的下半场,拼的不是模型多大,而是背后的语义层有多扎实。


体验与获取