数据库中的数据建模:范式、反范式、宽表与指标口径
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 用维度模型和宽表提升查询效率。
- 历史分析需要快照字段。
- 指标系统需要统一口径。
- 数据平台需要血缘治理。
表设计不是孤立工作,它连接业务语义、查询性能和数据可信度。