数据库中的数据建模:范式、反范式、宽表与指标口径

OLTP 建模追求一致性和约束,OLAP 建模追求查询效率和指标稳定,两者的设计目标完全不同。

数据库中的数据建模:范式、反范式、宽表与指标口径

数据建模不是画几张表。它决定了系统怎么写入、怎么查询、怎么演进,也决定了后续报表和指标是否可信。

OLTP 和 OLAP 的建模目标完全不同。业务库更关心正确性,分析库更关心查询效率和口径一致。

Rendering diagram...

OLTP:范式建模

业务库通常会拆表。订单主表、订单明细、支付流水、退款记录、物流记录分开保存。

CREATE TABLE orders (
  id BIGINT PRIMARY KEY,
  user_id BIGINT NOT NULL,
  status VARCHAR(32) NOT NULL,
  created_at TIMESTAMP NOT NULL
);

CREATE TABLE order_items (
  id BIGINT PRIMARY KEY,
  order_id BIGINT NOT NULL,
  sku_id BIGINT NOT NULL,
  quantity INT NOT NULL,
  amount DECIMAL(12, 2) NOT NULL
);

这种设计减少冗余,方便约束和事务处理。但如果用它直接跑报表,就会有大量 Join。

OLAP:维度建模

分析系统通常围绕事实表和维度表建模。

事实表记录业务事件,比如订单明细、支付流水、用户行为事件。

维度表描述分析视角,比如用户、商品、渠道、地域、日期。

订单事实表
  ├── 用户维度
  ├── 商品维度
  ├── 渠道维度
  └── 日期维度

星型模型把事实表放中间,维度表围绕事实表展开。它适合 BI 和多维分析。

宽表为什么常见

宽表是把多个维度字段冗余到事实表里,减少查询时 Join。

例如订单明细宽表里直接保存商品名称、类目、渠道、地区、用户等级。

CREATE TABLE order_detail_wide (
  order_id BIGINT,
  order_item_id BIGINT,
  user_id BIGINT,
  user_level VARCHAR(32),
  sku_id BIGINT,
  product_name VARCHAR(256),
  category_name VARCHAR(128),
  channel VARCHAR(64),
  pay_amount DECIMAL(12, 2),
  pay_time TIMESTAMP
);

宽表牺牲了一些存储空间,换来更简单、更快的查询。

快照字段

分析历史数据时,维度变化是大问题。

如果商品今天属于“手机”,明天调整到“数码”,历史销售额应该按哪个类目算?

通常做法是在事实表里保存业务发生时的维度快照。订单发生时的商品名、类目、价格、用户等级,都应该保留下来。

指标口径

指标口径比表结构更重要。

GMV 是否包含未支付订单?是否扣除退款?优惠券是否算入销售额?取消订单是否计入下单量?

这些问题如果没有统一定义,不同团队会算出不同结果。

一个指标定义可以写成:

metric: paid_gmv
name: 支付 GMV
formula: sum(pay_amount)
filters:
  - pay_status = 'PAID'
  - order_status not in ('CANCELLED')
time_field: pay_time
owner: data-team

数据血缘

高级数据系统需要知道一个指标从哪里来。

比如“渠道支付 GMV”来自哪些源表、经过哪些任务、写入哪些聚合表、被哪些报表使用。这就是数据血缘。

没有血缘,字段改动很容易影响下游,但没人知道。

小结

数据建模要分场景:

  • OLTP 用范式化模型保证一致性。
  • OLAP 用维度模型和宽表提升查询效率。
  • 历史分析需要快照字段。
  • 指标系统需要统一口径。
  • 数据平台需要血缘治理。

表设计不是孤立工作,它连接业务语义、查询性能和数据可信度。

参考链接