# 1 - 电商问数：项目概述与数仓基础

---

**本章课程目标：**

- 理解「电商问数」要解决的核心问题，以及它为什么属于典型的 `NL2SQL` 问数项目。
- 区分**业务数据库**与**数据仓库**的定位差异，并建立“为什么企业分析通常不直接查业务库”的基础认识。
- 初步掌握**事实表**、**维度表**和**维度建模**的核心概念，同时对后续章节要解决的问题形成整体预期。

**学习建议：** 这一章先把业务问题看清楚，再看技术名词。重点理解自然语言问数为什么需要数据仓库、维度建模和指标口径支撑。读完后最好能用一个例子说清“事实表、维度表、指标”各自解决什么问题，这会直接影响后面你能不能看懂字段召回和 SQL 生成。

---

## 1、本章导读

### 1.1 项目简介

「电商问数」是一个面向企业数据分析场景的智能体项目。它要解决的问题非常明确：让用户不用手写 SQL，也能像聊天一样从数据仓库里获取想要的分析结果。

如果你是开发者、产品经理、运营，甚至是业务同学，通常都会遇到这样一种情况：

- 我想知道华北地区的销售总额是多少。
- 我想看各品牌销量最高的商品有哪些。
- 我想统计男性用户的订单总额。

这些问题本质上都不是“聊天问题”，而是**数据查询问题**。传统做法往往需要懂 SQL 的开发或数据分析师来完成；而「电商问数」要做的，就是把“自然语言提问”自动转换成“可执行的 SQL 查询”，再把查询结果返回给用户。

一句话概括，这个项目就是一个典型的 `NL2SQL` 智能体项目。`NL2SQL` 的意思是：`Natural Language to SQL`，也就是“自然语言转 SQL”。用户只负责提问，系统借助大模型理解问题并生成 SQL，再到数据库或数据仓库中执行查询，最后把结果返回给用户。

**一句话记忆：** 「电商问数」要做的事，就是把“人能看懂的问题”翻译成“数据库能执行的 SQL”。

![电商问数查询结果页：展示 LangGraph 执行流程、SQL 闭环进度和最终 GMV 查询结果](./images/0/0-3.png)

---

## 2、问数项目背景

### 2.1 为什么企业需要「问数」系统

企业每天都在产生大量数据。以电商业务为例，用户在平台上的很多关键行为，都会沉淀成数据，例如：

- 浏览和收藏了哪些商品
- 把哪些商品加入购物车并最终下单
- 支付了多少钱，以及来自哪个地区、属于哪类用户

这些数据最开始是为了支撑业务系统本身而保存的。比如：

- 订单要保存下来，用户之后才能查看自己的订单记录
- 购物车要保存下来，用户下次打开 App 时才能看到之前加入的商品
- 用户信息要保存下来，系统才能识别当前用户是谁

但数据的价值显然不止于“把业务跑起来”。

当数据积累到一定规模后，我们还希望基于这些数据做进一步分析，例如：

- 哪个地区的销售额更高
- 哪些用户群体更有购买倾向，分析用户偏好
- 哪个品牌最近表现最好

这类问题已经不属于日常业务操作，而属于数据分析与经营决策的范畴。企业做“问数”系统，本质上就是为了把数据分析的门槛降下来，让更多人能够直接使用数据。

---

## 3、数据仓库基础

正式看事实表和维度表之前，先把几层数据关系放到一张图里。业务数据库负责支撑交易系统运行，教学数据仓库负责承接分析查询，元数据知识库则把“有哪些表、字段、指标和值”整理给问数智能体使用。

![业务数据库、教学数据仓库、元数据知识库和问数智能体之间的关系](images/1/1-3-0-1.svg)

### 3.1 为什么不直接查业务数据库

你可能会问：既然业务数据本来就保存在数据库里，为什么不直接查业务库，而要单独搞一个“数据仓库”？原因很简单，因为业务数据库和分析型查询的目标并不一样。

业务数据库的核心任务是支撑线上业务稳定运行。它更关注下面三件事：下单要快，支付要快，查询订单详情要快。

而分析型查询通常具有下面这些特点：需要扫描大量数据，并做聚合计算，比如 `sum`、`count`、`avg`；经常要做多表关联、分组和排序；查询更重，对计算资源的消耗也更高。

如果把大量分析 SQL 直接跑在业务数据库上，就可能带来明显的问题：数据库负载升高；线上接口响应变慢；严重时甚至影响正常业务。

所以在真实企业环境里，通常会把业务数据同步到一个专门用于分析的系统中，这个系统就是**数据仓库**。

### 3.2 什么是数据仓库

你可以先把“数据仓库”理解成一个专门存放分析数据的大仓库。

它有三个关键特征：

1. 它存的是来自业务系统的大量历史数据。
2. 它主要服务于统计分析，而不是直接服务于线上交易。
3. 它的数据组织方式，通常会为了“分析方便”而重新设计。

这意味着，数据仓库并不是把**业务数据库**原封不动搬过来，而是会在同步过程中进行整理、建模和加工，让后续分析更高效、更清晰。

在大数据体系里，数据仓库常见的实现方式有很多，比如 `Hive`。你可以先把 `Hive` 理解成一个面向数据仓库场景的分析引擎。它表面上也是让我们写 SQL，但底层会把这些 SQL 转换成分布式计算任务，交给大数据集群去执行，从而处理更大规模的分析任务。

不过本教程不会真的去部署 `Hive`。原因很简单：它通常还依赖 `Hadoop` 一类的大数据基础设施，环境较重，初学时很容易把注意力放到平台搭建上，而不是问数系统本身。

基于这个考虑，当前教学环境直接使用 `MySQL` 来模拟数据仓库场景。虽然底层没有真正使用 `Hive`，但项目的核心链路并没有变化，依然是**生成 SQL、执行 SQL、返回分析结果**。这样既能保留问数系统的核心思路，也能把环境复杂度控制在更适合学习的范围内。

所以可以把本项目理解为：用更容易上手的方式，完整体验一套企业级问数系统的核心设计。

> **简单来说：**
> 数据仓库中的数据通常**来源于业务数据库**，但并不是把业务库中的表和数据原封不动、一比一地搬过来。
> 它通常会根据分析场景进行**清洗、整理和重新建模**。
> **业务数据库**主要是为了支撑日常业务运行，**数据仓库**则主要是为了做统计分析、数据洞察和业务决策。

### 3.3 业务数据库和数据仓库区别

如果前面几段你已经看懂了，可以再用下面这张表快速对照一次：

| 对比项   | 业务数据库                       | 数据仓库                                     |
| -------- | -------------------------------- | -------------------------------------------- |
| 主要目标 | 支撑日常业务运行                 | 支撑统计分析与经营决策                       |
| 常见场景 | 下单、支付、查订单、改库存       | 按地区统计销售额、按品牌分析销量、做用户画像 |
| 常见类型 | 通常是关系型数据库，比如 `MySQL` | 常见实现有 `Hive`，本项目中用 `MySQL` 来模拟 |
| 查询特点 | 单条查询多、写入频繁、强调高并发 | 聚合计算多、扫描数据量大、多表关联多         |
| 设计重点 | 保证一致性、响应快、写入稳       | 方便统计、方便分组、方便分析                 |
| 数据来源 | 直接来自业务系统                 | 通常来源于业务系统同步后的整理结果           |
| 建模方式 | 更偏规范化设计                   | 更常见维度建模                               |

**一句话记忆：** 业务数据库是为了把业务“跑起来”，数据仓库是为了把数据“用起来”。

### 3.4 数据仓库为什么更适合分析

数据仓库之所以适合分析，不只是因为它和业务库分开了，更重要的是它通常会采用更适合分析的建模方式。

在业务数据库中，表结构往往遵循关系型数据库的规范化设计，目标是：

- 减少冗余
- 保证数据一致性
- 服务日常业务写入与查询

但在分析场景中，我们更看重的是：

- 指标能不能方便统计
- 维度能不能方便筛选和分组
- SQL 能不能写得清楚、稳定、好维护

因此，数据仓库中常见的一种建模方式叫做**维度建模**。

这里也可以顺带建立一个更清楚的理解：从业务数据库到数据仓库，通常还会经历抽取、清洗、转换等数据加工过程，而**建模**更贴切地说，是其中“根据分析需求重新设计表结构”的这一步。

---

## 4、维度建模入门

### 4.1 维度建模简介

维度建模是数仓里非常经典的一种设计方法。**简单来说，它就是围绕“分析一件业务事件需要看哪些指标、从哪些角度观察”来设计表结构。**

在这种设计里，表通常会被分成两大类：事实表、维度表。

可以先把握这两个核心定义：

- 事实表：记录“发生了什么事”
- 维度表：描述“这件事发生在什么对象、什么时间、什么地点、什么属性下”

### 4.2 常见建模方式

在企业数据体系里，建模方式并不只有维度建模。不同建模方式，通常服务于不同目标。

| 建模方式                | 主要面向什么场景                 | 核心特点                                    | 适不适合本项目                     |
| ----------------------- | -------------------------------- | ------------------------------------------- | ---------------------------------- |
| `ER` 建模 / 规范化建模  | 业务数据库、交易系统             | 强调减少冗余、保证一致性，典型是 `3NF` 设计 | 不太适合作为问数系统的主模型       |
| 维度建模                | 数据仓库、报表分析、BI、问数系统 | 把表拆成事实表和维度表，查询思路清晰        | **最适合本项目**                   |
| `Data Vault` 建模       | 企业级数仓底层、复杂多源接入     | 强调可追溯、可扩展、便于持续接入新数据      | 更适合底层建设，不适合作为入门主线 |
| 宽表建模 / 反规范化建模 | 固定报表、高频分析查询           | 把常用字段尽量提前摊平，减少查询时的 `join` | 可作为补充手段，但不是本项目重点   |

先把它们之间的关系记成一句话会更方便：**业务库更常见的是 ER/规范化建模，分析场景更常见的是维度建模，而大型企业数仓底层有时也会使用 Data Vault 建模。**

本教程之所以重点讲维度建模，是因为「电商问数」要解决的是“如何更自然地做统计分析和生成 SQL”，而在这个目标下，维度建模最直观，也最适合作为分析型查询的入门思路。

### 4.3 当前项目数仓模型说明

下面这张图展示的是当前项目中，数据仓库采用维度建模后的事实表与维度表示例。可以结合这张简化示意图理解它们之间的关系：

![电商问数教学数仓星型模型：事实表 fact_order 与 dim_customer、dim_product、dim_region、dim_date 等维度表关系](images/1/1-4-2-1.png)

在本项目的教学环境中，我们用一张事实表 `fact_order` 和四张维度表 `dim_product`、`dim_customer`、`dim_region`、`dim_date` 来模拟一个简化后的电商数仓。

为了方便看懂这张图，这里先补充两个图例说明：

- **钥匙图标**：表示该字段是**主键**，用来唯一标识表中的一条记录
- **红色圆点**：表示该字段是**重点维度字段**，通常会作为筛选条件、分组字段，后续也会重点关注它们的字段取值

如果按字段角色来拆分，这个模型可以这样理解：

- `fact_order`（事实表 订单信息）：`order_id` 是主键；`customer_id`、`product_id`、`date_id`、`region_id` 是外键；`order_quantity`、`order_amount` 是度量字段
- `dim_product`（维度表 商品信息）：`product_id` 是主键；`product_name`、`category`、`brand` 是维度字段
- `dim_customer`（维度表 客户信息）：`customer_id` 是主键；`customer_name`、`gender`、`member_level` 是维度字段
- `dim_region`（维度表 地区信息）：`region_id` 是主键；`province`、`region_name`、`country` 是维度字段
- `dim_date`（维度表 时间信息）：`date_id` 是主键；`year`、`quarter`、`month`、`day` 是维度字段

**维度表通常是“1 个主键 + 多个维度字段”，事实表通常是“1 个主键 + 多个外键 + 多个度量字段”。**

其中，`fact_order` 是核心事实表，它记录的是“下单”这件业务事件。每一行数据都对应一次订单行为，里面既包含关联各维度的外键，也包含可以统计的数值字段，例如：

- 四个外键：`customer_id`、`product_id`、`region_id`、`date_id`，分别用来关联客户、商品、地区和日期维度
- 一个数量字段：`order_quantity`，表示这笔订单买了多少件商品
- 一个金额字段：`order_amount`，表示这笔订单的总金额

这里的 `order_quantity` 和 `order_amount` 这类数值字段，通常也叫度量值。它们正是后续统计分析时最常被聚合计算的对象。

而维度表的作用，是补充描述信息。例如，`dim_product` 表用来描述商品名称、品类和品牌，`dim_customer` 表用来描述客户姓名、性别和会员等级，`dim_region` 表用来描述省份、大区和国家，`dim_date` 表用来描述年、季度、月、日等时间属性。

这里还需要特别说明一点：当前教程中的模型是一个为了教学而简化过的数仓示例。在真实企业的数据仓库中，通常不会只有一张事实表。因为企业中的业务事件往往不止“下单”这一种，还可能包括加购、支付、退单、收藏、浏览等多种行为。一般来说，每一种核心业务事件都可能对应一张独立的事实表。

也就是说，真实数仓里常见的情况往往是：

- 有多张事实表，分别记录不同类型的业务事件
- 这些事实表会共用一部分维度表
- 用户、商品、地区、时间等维度，往往会在多个事实表之间重复使用

例如，订单事实表、加购事实表、退单事实表，可能都会关联同一张客户维度表、同一张商品维度表、同一张地区维度表和同一张日期维度表。这样设计的好处是，不同业务事件可以分别统计，但观察这些事件时使用的分析视角又是统一的。

而在本教程里，为了让大家先把核心思路学明白，我们只保留了一张 `fact_order` 事实表和四张常用维度表。虽然模型被简化了，但它已经足够帮助我们理解智能问数系统最核心的分析逻辑。

### 4.4 当前项目数仓的表示例

如果你想再具体一点，先看看当前教学环境中的 4 张典型数仓表。下面每张表都会展示完整字段名作为表头，并给出 3 行示例数据，帮助你先建立直观认识。

**`dim_customer` 示例数据：**

| customer_id（客户 ID，主键） | customer_name（客户名称，维度字段） | gender（性别，维度字段） | member_level（会员等级，维度字段） |
| ---------------------------- | ----------------------------------- | ------------------------ | ---------------------------------- |
| `C001`                       | `李伟`                              | `男`                     | `黄金`                             |
| `C002`                       | `王芳`                              | `女`                     | `白银`                             |
| `C003`                       | `张敏`                              | `女`                     | `铂金`                             |

**`dim_product` 示例数据：**

| product_id（商品 ID，主键） | product_name（商品名称，维度字段） | category（品类，维度字段） | brand（品牌，维度字段） |
| --------------------------- | ---------------------------------- | -------------------------- | ----------------------- |
| `P001`                      | `iPhone 15 Pro`                    | `手机数码`                 | `苹果`                  |
| `P002`                      | `Galaxy S24 Ultra`                 | `手机数码`                 | `三星`                  |
| `P003`                      | `戴森 V15 吸尘器`                  | `家用电器`                 | `戴森`                  |

**`dim_region` 示例数据：**

| region_id（地区 ID，主键） | province（省份，维度字段） | region_name（大区名称，维度字段） | country（国家，维度字段） |
| -------------------------- | -------------------------- | --------------------------------- | ------------------------- |
| `R001`                     | `广东省`                   | `华南`                            | `中国`                    |
| `R002`                     | `浙江省`                   | `华东`                            | `中国`                    |
| `R003`                     | `北京市`                   | `华北`                            | `中国`                    |

**`fact_order` 示例数据：**

| order_id（订单 ID，主键） | customer_id（客户 ID，外键） | product_id（商品 ID，外键） | date_id（日期 ID，外键） | region_id（地区 ID，外键） | order_quantity（下单数量，度量字段） | order_amount（下单金额，度量字段） |
| ------------------------- | ---------------------------- | --------------------------- | ------------------------ | -------------------------- | ------------------------------------ | ---------------------------------- |
| `ORD20250101001`          | `C001`                       | `P001`                      | `20250101`               | `R001`                     | `1`                                  | `8999.0`                           |
| `ORD20250102001`          | `C002`                       | `P002`                      | `20250102`               | `R002`                     | `2`                                  | `6999.0`                           |
| `ORD20250103001`          | `C003`                       | `P003`                      | `20250103`               | `R003`                     | `1`                                  | `3999.0`                           |

先记住一句话就够了：**事实表负责记录业务事件，维度表负责补充事件的观察角度。** 后面在第 2 章讲到同步脚本、元数据知识库和字段召回时，本质上都是围绕这些原始数仓表展开的。

### 4.5 用几个问题理解事实表和维度表

假设我们要问这样几个问题：各地区销售总额是多少？各品牌销售总额是多少？男性用户的订单总额是多少？

你会发现，这些问题虽然表面不同，但写 SQL 的思路很相似：

1. 先找到承载核心指标的事实表，比如 `fact_order`
2. 再根据分析维度，关联对应的维度表
3. 最后做筛选、分组、聚合统计

例如：

- 统计各地区销售额，要关联 `dim_region`
- 统计各品牌销售额，要关联 `dim_product`
- 统计男性用户订单总额，要关联 `dim_customer`

如果你想先快速抓住这三个问题的共性，也可以先看下面这张总表：

| 问题             | 事实表       | 维度表         | 关联字段                                            | 统计字段                  | 聚合方式 |
| ---------------- | ------------ | -------------- | --------------------------------------------------- | ------------------------- | -------- |
| 各地区销售总额   | `fact_order` | `dim_region`   | `fact_order.region_id = dim_region.region_id`       | `fact_order.order_amount` | `sum`    |
| 各品牌销售总额   | `fact_order` | `dim_product`  | `fact_order.product_id = dim_product.product_id`    | `fact_order.order_amount` | `sum`    |
| 男性用户订单总额 | `fact_order` | `dim_customer` | `fact_order.customer_id = dim_customer.customer_id` | `fact_order.order_amount` | `sum`    |

如果结合上面的示意图来看，这几个问题可以进一步拆成下面这样的查询思路：

- **各地区销售总额是多少**

先从 `fact_order` 中取出订单金额字段 `order_amount`，再通过 `fact_order.region_id = dim_region.region_id` 关联地区维度表 `dim_region`，按 `dim_region.region_name` 分组，最后对 `fact_order.order_amount` 做 `sum` 聚合。

- **各品牌销售总额是多少**

这个问题和上一个问题的思路完全类似，只不过分析维度从“地区”变成了“品牌”。因此仍然从 `fact_order` 出发，使用 `fact_order.product_id = dim_product.product_id` 关联商品维度表 `dim_product`，按 `dim_product.brand` 分组，再对 `fact_order.order_amount` 做 `sum` 聚合。

- **男性用户的订单总额是多少**

这个问题的关键在于“男性用户”是一个筛选条件，而不是分组维度。因此我们还是从 `fact_order` 出发，通过 `fact_order.customer_id = dim_customer.customer_id` 关联客户维度表 `dim_customer`，然后用 `dim_customer.gender = '男'` 作为过滤条件，最后对 `fact_order.order_amount` 做 `sum` 聚合。

如果将来问题中再加上时间条件，比如“统计 2024 年各地区销售总额”，那就在原有基础上再关联 `dim_date`，通过 `fact_order.date_id = dim_date.date_id` 关联后，使用 `dim_date.year = 2024` 作为过滤条件即可。

这就是维度建模的价值所在：它让分析型 SQL 的结构更稳定、思路更统一，也更适合交给智能体去理解和生成。

如果把上面的例子再压缩成一个通用模板，其实大多数分析型 SQL 都可以按下面这个思路去理解：

```sql
select 维度字段, 聚合函数(度量字段)
from 事实表
join 维度表 on 事实表.外键 = 维度表.主键
where 过滤条件
group by 维度字段
```

在「电商问数」里，大模型最终也是在做类似这样的事：先判断**事实表是谁**、**维度表是谁**、**过滤条件落在哪个字段上**，再把这些关系组织成 SQL。

---

## 5、项目定位

### 5.1 「电商问数」主要做了什么

理解了数据仓库和维度建模之后，我们再来看「电商问数」的核心目标，就会更清楚。

很多同学第一次接触 `NL2SQL` 时，都会先想到一个最直接的做法：把用户的问题直接交给大模型，再配上一句提示词，比如“请帮我把这个问题转换成 SQL”，让模型自己生成查询语句。

这个思路听起来很自然，但在真实项目里通常是不够的。原因很简单：大模型本身并不知道你当前的数据仓库里到底有哪些表、每张表里有哪些字段、字段之间如何关联、哪些字段是维度、哪些字段是度量值。如果缺少这些上下文，它就很难稳定地生成正确 SQL。

那我们是不是可以把整个数据仓库的表结构、字段信息一次性全都塞进提示词里，再交给大模型呢？这看起来像是一个解决办法，但实际也存在明显问题：

- 数据仓库中的表通常很多，字段也很多，提示词很容易变得过长
- 大模型的上下文长度有限，表和字段太多时容易超出上限
- 即使没有超出上限，过多的无关信息也会干扰模型判断，并拖慢响应速度

所以，真正可行的思路并不是“把所有表都交给大模型”，而是先为**数据仓库**构建一套**元数据知识库**，再围绕用户问题召回最相关的表、字段、字段取值和指标说明，然后把这些上下文交给大模型生成 SQL。

如果你现在对“元数据知识库到底是什么、它和数据仓库是什么关系、系统具体是怎么召回的”还没有完全看懂，也没有关系。这里先抓住主线就够了：**数据仓库负责存放分析数据，元数据知识库负责帮助智能体理解这些数据，智能体再基于两者完成查数。**

更细的系统架构、同步过程、召回机制和执行流程，会在下一章 [第 2 章 项目整体架构与智能体流程](/实战项目-电商问数/2-项目整体架构与智能体流程.md) 里展开说明。

比如用户输入：

- 统计华北地区的销售总额
- 统计各地区销量排行前三的商品

系统最终要做到的，不是泛泛而谈地“解释问题”，而是真正生成可以执行的 SQL，并给出可靠结果。这就是“问数智能体”和普通聊天助手最大的区别。

也可以把它看作一种**先检索相关元数据，再生成 SQL** 的思路。后面讲系统架构时，我们再正式展开它和 `RAG` 思路之间的关系。也正因为如此，本项目后续的重点不只是“怎么调用大模型”，更重要的是“怎么把元数据组织好、检索好、利用好”。

### 5.2 一个最小可理解的问数链路

如果你想先用一句话把整个项目记住，可以先记下面这条最小链路：

1. 用户先提一个自然语言问题，例如“统计华北地区的销售总额”。
2. 系统先从元数据知识库里找出和这个问题最相关的字段、字段值和指标定义。
3. 大模型基于这些上下文生成 SQL。
4. 系统先校验 SQL，必要时再自动纠错。
5. SQL 通过后再真正执行，并把查询结果返回给用户。

这条最小链路，其实就是整套项目后续章节的主线：**用户提问，系统先找知识，再生成 SQL，最后执行查询。**

### 5.3 本教程你将学到什么

通过这个项目，你将不只是学会“做一个能聊天的页面”，而是会系统接触到企业级智能体项目中的一整套关键能力：

- 如何围绕真实数仓场景设计一套可用的元数据知识库
- 如何把字段、指标和字段取值召回出来，并整理成适合大模型使用的上下文
- 如何用结构化智能体流程把生成、校验、执行和前后端交互串起来

如果你之前没有做过数据仓库项目，也不用担心。本教程会尽量按照“先理解场景，再理解设计，最后理解代码”的顺序展开，尽量让每一步都能跟得上。

---

**本章小结：**

- 问数系统的目标，是把**自然语言提问**转成**可执行的 SQL 查询**。
- 企业分析通常不会直接查业务数据库，而是会把数据整理进更适合分析的数据仓库，并通过维度建模让统计逻辑更清晰。
- 「电商问数」的关键，不是让大模型盲猜 SQL，而是先通过元数据知识库把相关上下文准备好，再让模型生成 SQL。

到这里，第 1 章的内容就结束了。接下来我们会进入下一份文档《第二章 项目整体架构与智能体流程》，系统梳理「电商问数」的元数据知识库设计、索引设计、同步脚本以及问数智能体流程。
