4. 维度建模基础与Databricks实现

维度建模的目标,是把业务过程拆成“可度量的事实”和“描述事实的维度”,让分析人员能够用简单的连接查询回答“谁、在什么时间、通过什么渠道、买了什么、买了多少”。在 Databricks 上,这一目标通常落在 Delta 表上:事实表记录业务事件的度量与外键,维度表记录实体的描述属性,二者通过代理键关联。本节以销售主题为例,说明星型模型与雪花模型的取舍、事实表与维度表的设计要点,并给出可直接运行的 Delta 建表与装载示例。

一、星型模型与雪花模型:先定结构,再定表

星型模型以事实表为中心,维度表直接挂在事实表周围,维度不再继续向外拆分。雪花模型则把维度进一步规范化,例如把“产品”拆成产品表与类别表,把“门店”拆成门店表与区域表。两者的核心差异不在“好不好看”,而在查询路径长度与维护成本:星型模型连接少、查询简单、适合 BI 高频聚合;雪花模型冗余低、更新一致性好,但连接层级多,查询更复杂。

在 Databricks 中,Delta 表支持 ACID 事务、时间旅行与 Schema 演进,因此星型模型并不需要为了“避免冗余”而过度拆分。对于销售主题,推荐以星型模型为主:fact_sales 作为事实表,dim_date、dim_product、dim_store、dim_customer 作为维度表。若产品类别需要独立维护且被多个事实表共享,可将其作为雪花分支保留,但不要为了规范化而把常用维度拆得过细。

销售主题星型模型(含雪花分支) 事实表居中,维度表直连;产品类别作为可选雪花分支 fact_sales sales_sk (代理键) date_sk / product_sk store_sk / customer_sk quantity, amount, cost dim_date date_sk (PK) year, quarter, month weekday, is_holiday dim_product product_sk (PK) product_name, brand category_id (FK) dim_store store_sk (PK) store_name, city region, open_date dim_customer customer_sk (PK) customer_name, level register_date, phone dim_category(雪花) category_id (PK), category_name date_sk product_sk store_sk customer_sk category_id 图例 事实表(度量+外键) 维度表(描述属性) 雪花分支(可选)

二、事实表设计:粒度、度量与代理键

事实表设计的第一步是确定粒度。销售主题最常用的粒度是“订单行”,即一张订单中的一个商品行。粒度决定了事实表的唯一性:如果同一订单同一商品可能分多次发货,就需要把粒度细化到“发货行”,否则度量会被重复计算。在 Databricks 中,可以用 MERGE INTO 按业务主键做幂等写入,避免重复装载。

事实表的字段分为三类:外键、度量和退化维度。外键指向各维度表的代理键;度量是可加、半可加或不可加的数值,如 quantity(可加)、amount(可加)、discount_rate(不可加,需按行计算);退化维度是订单号、发票号等不单独建维但常被查询的字段。代理键建议用 BIGINT 并由 monotonically_increasing_id() 或维度表生成,避免直接使用业务键导致维度缓慢变化时事实表需要重写。

-- 事实表:销售订单行
CREATE TABLE IF NOT EXISTS sales_gold.fact_sales (
  sales_sk        BIGINT GENERATED ALWAYS AS IDENTITY,
  order_no        STRING,
  date_sk         INT,
  product_sk      BIGINT,
  store_sk        BIGINT,
  customer_sk     BIGINT,
  quantity        INT,
  unit_price      DECIMAL(10,2),
  discount_rate   DECIMAL(4,3),
  amount          DECIMAL(12,2),
  cost            DECIMAL(12,2)
)
USING DELTA
PARTITIONED BY (date_sk)
TBLPROPERTIES ('delta.autoOptimize.optimizeWrite' = 'true');

这里按 date_sk 分区,是因为销售查询几乎总带时间范围过滤,分区裁剪能显著减少扫描量。注意不要按 product_sk 这类高基数字段分区,否则会产生大量小文件。Delta 的 autoOptimize 可在写入时自动合并小文件,适合每日增量装载。

三、维度表设计:代理键、属性与缓慢变化维

维度表的核心是代理键与业务键的映射。以 dim_product 为例,product_sk 是代理键,product_id 是业务键,product_name、brand、category_id 是描述属性。当产品名称或品牌发生变化时,需要决定是覆盖旧值(SCD Type 1)还是保留历史版本(SCD Type 2)。销售分析通常需要按“下单时的产品属性”统计,因此推荐对关键属性使用 SCD Type 2,用 valid_from、valid_to、is_current 标记版本。

-- 维度表:产品(SCD Type 2)
CREATE TABLE IF NOT EXISTS sales_gold.dim_product (
  product_sk   BIGINT GENERATED ALWAYS AS IDENTITY,
  product_id   STRING,
  product_name STRING,
  brand        STRING,
  category_id  STRING,
  valid_from   DATE,
  valid_to     DATE,
  is_current   BOOLEAN
)
USING DELTA;

装载维度时,先用 MERGE 关闭旧版本,再插入新版本。判断“是否变化”应基于业务属性列,而不是整行比较,否则时间戳字段会导致误判。对于 dim_date,建议一次性生成 10 年日期,包含年、季度、月、周、星期、是否节假日等字段,避免在查询中反复调用日期函数。

四、从源表到星型模型的装载链路

在 Databricks 中,典型链路是 Bronze(原始订单)→ Silver(清洗与去重)→ Gold(星型模型)。Silver 层完成空值处理、金额校验、订单行拆分;Gold 层完成代理键查找与事实表写入。代理键查找可用广播连接:维度表通常较小,广播后与事实数据做 LEFT JOIN,把业务键替换为代理键。

-- 用维度代理键替换业务键,写入事实表
MERGE INTO sales_gold.fact_sales AS t
USING (
  SELECT
    s.order_no,
    d.date_sk,
    p.product_sk,
    st.store_sk,
    c.customer_sk,
    s.quantity,
    s.unit_price,
    s.discount_rate,
    s.quantity * s.unit_price * (1 - s.discount_rate) AS amount,
    s.cost
  FROM sales_silver.orders_clean s
  JOIN sales_gold.dim_date d
    ON s.order_date = d.date
  LEFT JOIN sales_gold.dim_product p
    ON s.product_id = p.product_id AND p.is_current = true
  LEFT JOIN sales_gold.dim_store st
    ON s.store_id = st.store_id
  LEFT JOIN sales_gold.dim_customer c
    ON s.customer_id = c.customer_id
) AS src
ON t.order_no = src.order_no AND t.product_sk = src.product_sk
WHEN MATCHED THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *;

执行后应检查三类问题:一是代理键为空,说明维度表缺少对应业务键,需要先补维度;二是金额为负或为零,说明折扣率或单价异常;三是同一订单行重复,说明 MERGE 的匹配键不完整。可以用 SELECT COUNT(*), COUNT(DISTINCT order_no, product_sk) FROM fact_sales 对比行数与唯一键数,若不一致则需回溯 Silver 层去重逻辑。

在 Databricks 中,维度表建议开启 delta.enableChangeDataFeed,便于下游增量消费维度变化;事实表则优先保证分区裁剪与文件合并,不必对每个度量列建索引。

五、设计检查清单

  • 事实表粒度是否唯一且与业务过程一致,是否存在重复计数风险。
  • 所有外键是否指向维度表的代理键,而非业务键。
  • 维度表是否包含 is_current 或版本字段,以支持历史属性分析。
  • 分区字段是否与高频过滤条件一致,是否避免高基数分区。
  • 金额、数量等度量是否在 Silver 层完成校验,Gold 层只做聚合与关联。
  • 是否用 MERGE 保证幂等,重复运行不会产生重复事实行。

星型模型与雪花模型并非互斥:在 Databricks 上,可以先用星型模型满足大多数分析,再对共享的、更新频繁的维度做雪花拆分。关键是让事实表保持窄而长、维度表保持宽而稳定,并用 Delta 的事务与时间旅行能力支撑可回溯的装载过程。