跳到正文

星型模型图:事实表与维度表,附销售示例

从业务问题出发设计销售星型模型:声明粒度,选定维度和事实,用 SCD 类型 2 保留客户历史,再进行查询。

· 创始人兼开发者

发布于

ER 图。fact_sales 位于中央,有五个外键、一个订单号和五个度量。鸦脚线把它连到上方的 dim_date、左侧的 dim_customer 和 dim_promotion,以及右侧的 dim_product 和 dim_store。下方的备注写明了粒度。

产品经理想要上个季度按类别和地区划分的收入,并与去年同期对比。在订单数据库里,这意味着要连接五张表,临时构建一个日历,还要记得客户会在不同地区之间搬家。每个新问题都需要新的连接,两位分析师还会算出两个不同的数字。

星型模型围绕一个业务流程重新组织数据,解决了这个问题:中央是一张存放测量值的事实表,周围是描述这些测量值的维度表。本文从粒度到 SQL,逐步设计一个销售星型模型,内容包括缓慢变化维度、星型与雪花模型的对比,以及列式数据仓库。

什么是星型模型

星型模型有两种表:

  • 事实表记录业务事件的测量值,例如一行订单明细:指向各维度的外键,加上数值度量。它们窄而长。
  • 维度表保存上下文,即谁、什么、哪里和何时:宽、扁平、行数少的表,包含类别或地区这类文本属性。

为这些技术中的许多命了名的 Kimball Group 把星型模型描述为:事实表通过主键和外键与维度表相连。Microsoft 的 Power BI 指南简洁地概括了它的吸引力:维度表支持筛选和分组,事实表支持汇总。

分四步设计

Kimball 的四步维度设计流程是按顺序做出的四个决定:

  1. 选择业务流程:门店和网店的零售销售。
  2. 声明粒度:每行订单明细一行,也就是一张订单中的一件商品。Kimball 把粒度称为有约束力的合约,要在任何维度或事实之前声明。
  3. 确定维度:日期、商品、门店、客户和促销,每个维度在每行订单明细上只有一个值。
  4. 确定事实:数量、单价、折扣、净额和成本,在这个粒度上都成立。

从原子粒度开始,也就是数据源能捕获的最低层级;汇总由原子行得出,绝不能反过来。永远不要混用粒度:按订单收取、却存在每一行上的运费,会按行数重复计算。像 Kimball 建议的那样,用业务规则把它分摊到各行,或者给它单独建一张事实表。

示例中的表

fact_sales 的列 角色 说明
date_key 外键 智能键,如 20261120
product_key, store_key, customer_key, promotion_key 外键 代理整数
order_number 退化维度 背后没有维度表
line_number 行标识 与 order_number 一起唯一
quantity, discount_amount, net_amount, cost_amount 可加度量 可沿任意维度求和
unit_price 不可加度量 不要求和

维度刻意做成扁平的:dim_product 在一行里带有品牌、子类别和类别,dim_store 把网店当作又一家门店,channel = 'online'。order_number 是退化维度:订单头的上下文在其他维度里,只剩下订单号,它对购物篮分组仍然有用。

单价是经典陷阱:跨行求和没有意义,应该改为用可加的部分相除,例如 SUM(net_amount) / SUM(quantity)。

代理键与日期维度

每个维度都有一个代理键,即从 1 开始顺序分配、本身没有含义的整数,因为记录历史的维度每个自然键对应多行,而且各数据源对键的看法常常不一致。把自然键(sku、store_code、customer_id)保留为属性,用来把行追溯到数据源。

日期维度是例外:它稳定且可预测,所以键可以有含义,例如 20261120,这也有利于分区。筛选和分组要用它的属性(year、quarter、is_holiday),而不是对键做算术运算。二十年只有约 7,305 行(20 × 365.25)。

事实表的外键不应为空:空键会让这一行在内连接中消失,总数会悄悄变小。对未知或不适用的值使用默认维度行;这里 promotion_key 为 0 表示「无促销」。

缓慢变化维度

缓慢变化维度(SCD)的类型决定属性变化后报表显示什么:

类型 名称 发生变化时 保留的历史
0 保留原值 值永不改变 仅原始值
1 覆盖 原地更新该行 无
2 新增行 关闭当前行,用新键插入新行 完整
3 新增属性 把旧值复制到 prior_ 列,再覆盖 一个先前的值

Kimball 的类型 4 到 7 增加了迷你维度以及这些类型的混合。

类型 2 是主力。客户 C-1042 于 2026 年 11 月 20 日从 Austin 搬到 Denver:

customer_key customer_id city valid_from valid_to is_current
1001 C-1042 Austin 2024-03-14 2026-11-20 false
2417 C-1042 Denver 2026-11-20 9999-12-31 true

之前的销售沿用键 1001,仍归在 Austin;之后的销售使用 2417。事实加载会选择范围包含订单日期的那一行(valid_from <= order_date < valid_to)。Kimball 对类型 2 的定义至少需要生效日期、失效日期和当前行标志;用遥远未来的 valid_to 代替空值,可以让范围筛选保持简单。dbt 快照可以替你实现类型 2。

要按当前城市汇报全部历史,就用相同的 customer_id 连接到当前行。变更数据捕获很适合为类型 2 供数,因为每次更新都以事件的形式到达;参见 change data capture 图。

星型模型、雪花模型与宽表

星型模型 雪花模型 宽表
维度数据 每个维度一张扁平表 规范化的链:商品、子类别、类别 复制到每一行事实上
每次查询的连接 每个用到的维度一个 每条链中的每张表一个 无
修正一个属性 更新一张维度表 更新链中的一张表 重写每一行受影响的事实
最适合 大多数数据集市和 BI 模型 对分析很少值得 一个仪表板或 ML 抽取,由星型模型生成

Kimball 不建议使用雪花维度:业务用户更难浏览,可能损害查询性能,而扁平化的维度包含的信息完全相同。Power BI 的指南也持相同看法。

宽表不需要连接,列式压缩也让重复的值保持低成本。代价是维护:修正变成重写,新属性变成重建,所以要从星型模型生成它。

这个模型让查询变简单

SELECT d.year_month,
       p.category,
       SUM(f.net_amount)                                AS revenue,
       SUM(f.net_amount - f.cost_amount)                AS gross_margin,
       SUM(f.net_amount) / NULLIF(SUM(f.quantity), 0)   AS avg_selling_price
FROM fact_sales AS f
JOIN dim_date    AS d ON d.date_key    = f.date_key
JOIN dim_product AS p ON p.product_key = f.product_key
JOIN dim_store   AS s ON s.store_key   = f.store_key
WHERE d.year = 2026
  AND s.channel = 'online'
GROUP BY d.year_month, p.category
ORDER BY d.year_month, revenue DESC;

每个连接都从事实表出发,通过一个整数键连到一个维度;筛选条件放在维度属性上,求和放在事实列上。新问题只改变 WHERE 和 GROUP BY,不改变连接,这就是 BI 工具能可靠生成这类查询的原因。

列式数据仓库中的性能

列式数据仓库只读取查询用到的列,这适合窄的事实表,小维度的连接成本也低:在广播连接中,BigQuery 会把较小的表发送到每个 worker。INT64 键和数量按 8 个逻辑字节、NUMERIC 金额按 16 个字节计算,上面的查询每行订单明细从事实表读取 64 字节,5 亿行约 32 GB,无论表有多宽。

  • 按日期裁剪。 按 date_key 对事实表分区、聚簇或排序。当分区列本身带有常量筛选条件时,BigQuery 才会裁剪分区,所以大规模扫描除了 dim_date.year,还应该用常量筛选 date_key。
  • 即使不强制,也要声明键。 BigQuery 使用声明为 NOT ENFORCED 的主键和外键来消除和重排连接,Snowflake 对标记为 RELY 的约束会消除连接。两者都不检查数据,违反约束可能返回错误结果,所以要在管道中测试键。
  • 在 Redshift 中同位存放。 Redshift 的指南是:按共同的键分布事实表和一个维度,对其他一些维度使用 ALL 分布,或者让 AUTO 来选择。

星型模型通常位于奖牌架构的金层;数据管道架构图展示数据如何到达那里,ETL 与 ELT则说明转换在哪里运行。

销售星型模型图

ER 图。fact_sales 位于中央,有五个外键、一个订单号和五个度量。鸦脚线把它连到上方的 dim_date、左侧的 dim_customer 和 dim_promotion,以及右侧的 dim_product 和 dim_store。下方的备注写明了粒度。

12 个组件 · 5 个连接

在 Archboard 中打开
图表内容
  • fact_sales
  • dim_date
  • dim_customer
  • dim_product
  • dim_promotion
  • dim_store
  • date_key
  • customer_key
  • promotion_key
  • product_key
  • store_key
以一行订单明细为粒度的销售星型模型。中央的事实表保存键和度量;周围的五张维度表保存查询用来筛选和分组的属性。客户维度以 SCD 类型 2 保留历史。

这张图的读法如下:

  • 中央的 fact_sales 有五个外键、退化的 order_number、line_number 和五个度量。
  • 每条线上都有鸦脚:一端是零行或多行事实,另一端是恰好一行维度。标签标明连接键。
  • 青绿色卡片是维度,dim_date 在上方,其余在左右两侧。
  • 琥珀色卡片 dim_customer 以 SCD 类型 2 保留历史。
  • Markdown 块写明粒度,以及哪些度量可以求和。

把星型模型图画清楚

  • 把粒度写在图上。 评审的人最先问的就是它,粒度被误读会导致重复计算。
  • 把事实放在中间,每个维度只隔一跳。 两个维度之间的连线就是雪花;要么有意地画出来,要么把它扁平化。
  • 标出键的角色:PK、自然键用 NK、退化维度用 DD。
  • 标出 SCD 类型:在保留历史的维度上。
  • 省去:加载作业、暂存表,以及没人用来筛选的列。
  • 每张图只画一个业务流程。 当多张事实表共享一致性维度时,加一个总线矩阵:流程为行,维度为列。

鸦脚记法请参见 ER 图示例。

在 Archboard 中构建

  1. 用 Google 或 GitHub 登录并创建一张图表。
  2. 如果表已经存在,把它们的 CREATE TABLE 语句粘贴到 SQL 转 ER 图工具,或把 DBML 粘贴到 DBML 转 ER 图工具,得到初稿。
  3. 从实体关系包中添加一个名为 fact_sales 的实体,每行一个属性:五个标记为 FK 的键、标记为 DD 的 order_number、line_number,然后是各个度量。给它蓝色强调色。
  4. 再添加五个实体节点:dim_date 在事实上方,dim_customer 和 dim_promotion 在左侧,dim_product 和 dim_store 在右侧。把代理键标为 PK,自然键标为 NK;使用青绿色强调色,dim_customer 用琥珀色。
  5. 用鸦脚把 fact_sales 连到每个维度,事实一端为零或多,维度一端为恰好一个,并在每条线上标出它的键。
  6. 添加基础包中的文本注释:键图例、SCD 类型 2 规则、智能日期键、默认促销行和网店。
  7. 在事实下方添加基础包中的 Markdown 块,写明粒度和可加性规则。

这个示例共用 7 个图表节点,在 Free 套餐的限制之内。

常见问题

简单来说,星型模型是什么? 一张中央事实表,保存销售金额之类的测量值,连接到描述它们的维度表,例如日期、商品和客户。画出来像一颗星。

事实表和维度表有什么区别? 事实表按声明的粒度存储事件的测量值,外加外键。维度表存储你用来筛选和分组的属性,例如类别或地区。

星型模型和雪花模型有什么区别? 星型模型把每个维度放在一张扁平表里;雪花模型把它规范化为商品、子类别和类别这样的链。星型模型需要的连接更少,也更易用。

星型模型是规范化的吗? 不完全是。维度表是刻意反规范化的,会在每个商品行上重复类别名称之类的值,而事实表保持窄。

在列式数据仓库中,星型模型还重要吗? 重要。宽表在列式引擎中扫描成本低,但星型模型仍然让你有一个地方修正属性,通过缓慢变化维度保留历史,并让各报表给出一致的答案。

免费开始 — 1 个席位,无需信用卡。

相关文章

← 返回博客

一分钟打开画布

使用 Google 或 GitHub 登录。单席位免费,团队加入后再升级。