数据仓库入门:从超市小票看懂数仓
很多同学第一次听到“数据仓库”四个字时,脑袋里浮现的是一排闪着蓝光的机柜、像《黑客帝国》里瀑布般滚动的绿色代码,或者干脆把它等同于“更大的 MySQL”。其实,它既不是神秘的黑科技,也不是简单的“大表”。数据仓库(Data Warehouse,下文简称“数仓”)是一套专门用来回答“企业到底发生了什么”的方法论与工具集。为了把这套方法论写得既学术又接地气,本文把一次日常购物小票拆成三张图,用它们串起整个数仓的知识链。读完以后,你可以拿着小票给家人讲清楚:为什么超市要先结账再送积分、为什么电商大促前要“跑数”一整夜,以及为什么分析师总爱把“维度”挂嘴边。
目录
一、一张小票的旅程:业务系统->ODS->DW->DM
二、事实表:告诉系统“发生了什么”
三、维度表:告诉系统“谁、在哪里、是什么”
四、桥接表:解决“多对多”难题
五、分层建模:ODS->DWD->DWS->ADS
一、一张小票的旅程:业务系统->ODS->DW->DM
我们先看看一张超市小票上到底写了什么:
“2025-09-04 20:31,门店 A,顾客 135****8888,购买 5 件青岛纯生 500 ml,单价 6.5 元,实付 32.5 元,使用 5 元券,积分 27。”
这条记录首先落在超市的 POS 系统里,属于实时交易流水。POS 系统不关心历史趋势,它只关心库存别为负、收银别出错。为了做“昨天卖了多少啤酒”这种跨天分析,超市必须把这条记录搬到一个专门分析用的库——这就是数仓的起点。整条链路可以概括为:
层级 | 中文名 | 存储内容 | 面向对象 | 技术示例 |
---|---|---|---|---|
ODS | 操作数据存储 | 原始流水,几乎不改 | 数据开发 | Kafka→Hive 外部表 |
DW | 数据仓库层 | 清洗、整合后的明细 | 数据分析师 | Hive/Spark SQL |
DM | 数据集市层 | 面向主题的汇总 | 业务人员 | ClickHouse/StarRocks |
ODS 层像快递站,只负责“原样签收”;DW 层像厨房,把菜择好、切好;DM 层像自助餐台,把菜分门别类摆好,等人取用。小票进入 DW 层后,会被拆成三张表:事实表、维度表、桥接表。下文用这张小票做范例,把这三张表讲清楚。
二、事实表:告诉系统“发生了什么”
事实表的核心是“可加数值”和“外键”。小票里的可加数值是“数量 5”“实付 32.5”“优惠 5”“积分 27”。外键则指向维度表:门店 A 对应门店维度表的一条记录,135****8888 对应顾客维度表的一条记录,青岛纯生 500 ml 对应商品维度表的一条记录。我们把这张事实表叫做 fct_sales
,核心字段如下:
字段 | 含义 | 示例值 |
---|---|---|
sales_id | 交易唯一键 | 2025090420310001 |
dt | 交易日期分区 | 2025-09-04 |
store_key | 门店维度键 | 1001 |
customer_key | 顾客维度键 | 987654 |
product_key | 商品维度键 | 556677 |
quantity | 销售数量 | 5 |
amt | 实付金额 | 32.5 |
coupon_amt | 优惠金额 | 5 |
point_amt | 积分 | 27 |
事实表的设计遵循“星型模型”:一张事实表周围环绕多张维度表,查询时通过外键 JOIN 即可。这样做的好处是查询逻辑清晰,坏处是维度表膨胀后 JOIN 代价高。业界用“分区+列存”解决:按 dt 做天级分区,再用 Parquet/ORC 列存,只扫需要的列。
三、维度表:告诉系统“谁、在哪里、是什么”
维度表保存“上下文”。没有维度表,事实表里的 1001、556677 只是一串冷冰冰的数字。维度表通常缓慢变化,所以 Kimball 提出“SCD”概念(Slowly Changing Dimension)。以商品维度为例:
字段 | 含义 | 示例值 |
---|---|---|
product_key | 代理键(无意义自增) | 556677 |
sku_code | 业务主键 | 6901234567890 |
sku_name | 商品名 | 青岛纯生 500 ml |
category_l1 | 一级品类 | 酒水 |
category_l2 | 二级品类 | 啤酒 |
brand | 品牌 | 青岛啤酒 |
unit_price | 当前售价 | 6.5 |
start_dt | 生效日期 | 2025-07-01 |
end_dt | 失效日期 | 9999-12-31 |
is_current | 是否当前版本 | Y |
当青岛啤酒 500 ml 在 10 月 1 日涨价到 7 元时,我们不修改旧记录,而是插入一条新记录,把旧记录的 end_dt 改为 2025-09-30,is_current 改为 N。这种叫 SCD Type 2,可保留历史版本,便于历史回溯。维度表虽然变化慢,但体积往往比事实表大,因为商品、门店、顾客的数量级远高于日交易笔数。
四、桥接表:解决“多对多”难题
小票里还有一个隐藏的多对多关系:优惠券。一张优惠券可能对应多件商品,一件商品也可能被多张券分摊。为了不在事实表里制造“coupon1,coupon2,coupon3”这种丑陋的数组,我们引入桥接表 bridge_sales_coupon
,字段如下:
字段 | 含义 | 示例值 |
---|---|---|
sales_id | 交易唯一键 | 2025090420310001 |
coupon_key | 优惠券维度键 | 888 |
discount_amt | 该券分摊金额 | 5 |
桥接表把事实表和维度表之间的多对多关系拆成一对多,既避免事实表过度宽化,又保留券粒度的分析能力。它本质上是“事实表的子表”,有时也被归到“事实表家族”。
五、分层建模:ODS->DWD->DWS->ADS
Kimball 的星型模型解决“怎么摆菜”,但企业级数仓还得回答“谁来择菜、谁来炒菜”。国内互联网公司普遍采用阿里提出的“四层模型”:
层级 | 中文名 | 作用 | 与星型模型对应 |
---|---|---|---|
ODS | 操作数据层 | 贴源不清洗 | 不做模型 |
DWD | 明细数据层 | 清洗、标准化 | 事实表+维度表 |
DWS | 服务数据层 | 轻度汇总 | 宽表/汇总表 |
ADS | 应用数据层 | 高度汇总 | 报表/接口 |
DWD 层的小票明细保留到分钟级粒度;DWS 层按“门店+品类”汇总到天;ADS 层直接吐出“昨日销售额”给老板手机。这样设计的好处是:下游改需求时,只需改 ADS 的 SQL,不必回头刷 ODS。
数据仓库不是一门高深莫测的技术,而是一套“把复杂留给自己,把简单留给业务”的工程哲学。下次当你在超市结账拿到小票时,不妨多看两眼:那条看似普通的记录,正在 ODS 层的 Kafka topic 里排队,等待被 DW 层的 Spark 任务拆成事实与维度,最终变成老板手机里的“昨日销售额”。理解了这条旅程,你就真正踏进了数据仓库的大门。祝学习愉快,欢迎把这篇博客转给同样对“数仓”二字心存敬畏的朋友。