第四章:库存管理中的维度建模
《数据仓库工具箱》第三版 — Ralph Kimball & Margy Ross
主题:Dimensional Modeling for Inventory
记录日期:2026-11-04
系列:维度建模读书笔记 · 第四章⚠️来源说明:本章按 Kimball 库存章的核心结论整理。标 ⚠️ 的两处(快照的多日期库龄扩展、DC/门店合并的具体取舍)是按维度建模原则做的延伸推断,建议对照原书确认。
导读:为什么库存是维度建模的"第二块试金石"
第三章零售回答的是"卖了多少",第四章库存回答的是"还剩多少"。
一句话概括两者的根本差别:
销售是流量,库存是存量。
这个差别不是文字游戏,它直接决定了:用哪类事实表、度量怎么聚合、BI 里怎么防止算错。第 2 章埋下的"三类事实表"和"半可加事实"两个伏笔,在这一章全部兑现。
本章的主线只有三条:
- 存量 vs 流量→ 决定主事实表类型(周期快照 vs 事务型)
- 半可加→ 决定聚合姿势(期末值/平均,不是 SUM)
- 结果 vs 原因→ 决定为什么要两张表(对账桥,不能互推)
一、库存建模:四步设计流程的应用
| 步骤 | 名称 | 库存中的选择 |
|---|---|---|
| 1 | 选择业务过程(Business Process) | 库存快照(核心)、收货、发货、调拨、盘点 |
| 2 | 声明粒度(Declare Grain) | 一行 = 一个产品 × 一个库存地点 × 一个快照日期 |
| 3 | 识别维度(Identify Dimensions) | 日期、产品、库存地点、事务类型、员工 |
| 4 | 识别事实(Identify Facts) | 期末库存数量、期末库存金额(成本口径) |
库存的黄金粒度:产品 × 库存地点 × 每日快照。
日粒度可以上卷到周/月/季,反过来永远不行;数据量 = 地点数 × SKU 数 × 天数,线性可预测、可控。
粒度声明要注意两点:
- 粒度是维度组合,不是度量。别把"库存数量"写进粒度里——它是挂在粒度上的事实。
- 说"库存地点"而不是"仓库",因为地点包含DC / 门店 / 在途三类,一张表才能装下全链路。
二、核心结构:库存周期快照事实表
FactInventorySnapshot(产品 × 地点 × 日) ├── 外键:date_key, product_key, location_key ├── 外键(可选):lot_key(批次/效期)、status_key(可用/冻结/在检) ├── 事实:qty_on_hand 期末库存数量 ← 半可加 │ inventory_value 期末库存金额(成本) ← 半可加 │ retail_value 期末库存零售金额 ← 可选 │ qty_in_transit 在途数量 ← 可选 └── 事实:qty_available / qty_frozen(可选,支撑可售性分析)| 要素 | 说明 |
|---|---|
| 事实表类型 | 周期快照——不是事务型,也不是累积快照 |
| 快照节奏 | 固定时刻(如每日 24:00)、固定频率,保证跨期可比 |
| 度量性质 | 半可加(semi-additive)——跨时间不可加 |
| 增长方式 | 地点数 × SKU 数 × 天数,线性且可预测 |
为什么最基础:库存是存量,只能靠"定期拍状态照"来记录;这正是第 2 章三类事实表里周期快照的教科书案例。
记录哪些度量:
- 期末数量(统一到最小库存单位)
- 期末金额(统一成本口径)
- 可选:零售金额、在途数量、可售/冻结数量
- 不记录:单价、周转率等不可加或比率型指标——它们放维度,或查询时现算
三、最关键的特性:半可加事实
┌────────────────────────────────────────┐ │ 库存度量的可加性 │ ├──────────────┬─────────────┬───────────┤ │ 完全可加 │ 半可加 │ 不可加 │ ├──────────────┼─────────────┼───────────┤ │ 销售数量 │ 库存数量 │ 单价 │ │ 销售金额 │ 库存金额 │ 周转率 │ │ 收货数量 │ 账户余额 │ 加价率 │ ├──────────────┼─────────────┼───────────┤ │ 任意维度可加 │ 仅时间不可加 │ 需先取分子 │ │ │ │ 分母再计算 │ └──────────────┴─────────────┴───────────┘3.1 三类可加性的精确定义
| 类型 | 定义 | 例子 |
|---|---|---|
| 完全可加 | 沿所有维度都能求和 | 销售数量、销售金额 |
| 半可加 | 沿除某一个维度外的所有维度都能求和;那个例外通常是时间 | 库存数量/金额、账户余额 |
| 不可加 | 沿任何维度直接求和都没意义 | 单价、周转率、毛利率 |
注意:半可加不是"不能随便加",而是"恰好只有一个维度不能加,而且那个维度是时间"。
3.2 为什么时间不可加
1/1 库存 100 件 1/2 库存 120 件 1/1 + 1/2 = 220 件 ← ❌ 胡说这 220 件里,1/1 的 100 件很可能就是 1/2 那 120 件里的大部分——同一批货在两天里被数了两遍。存量是"某个时点的状态",不是"这期间新产生的量"。把两个状态相加,等于把状态误当流量,结果必然重复计数。
3.3 正确的聚合姿势
| 需求 | 做法 |
|---|---|
| 时点库存 | 取期末值(period-end / closing balance),不能 SUM |
| 期间平均 | 期内各快照求平均(月均库存) |
| 跨产品 / 跨地点 / 跨类型 | 正常 SUM,怎么加都行 |
| 效率指标 | 周转率 = 销货成本 ÷ 平均库存;周转天数;售罄率 |
⚠️实操最大的坑:BI 工具默认对度量做 SUM。库存数量/金额必须在语义层显式标记为半可加,否则报表会静默算错——数字看着正常,结论全反。
口诀:跨产品跨门店可加,跨时间不可加;要期末值,或要平均值。
四、快照的延伸:多日期与库龄分析 ⚠️
⚠️ 本节为按建模原则的延伸推断,建议对照原书确认具体做法。
给库存快照挂上多个角色扮演的日期维度,可以把"存量"变成可分析"年龄"的模型:
FactInventorySnapshot ├── snapshot_date_key 快照日期(主日期) ├── last_receipt_date_key 最后收货日期 ┐ ├── last_sale_date_key 最后销售日期 ├─ 角色扮演 DimDate └── last_count_date_key 最后盘点日期 ┘有了这些日期,就能算:库龄(快照日 − 最后收货日)、滞销天数(快照日 − 最后销售日)、是否该降价清货、长期未盘点风险。这是"同一张日期维度表用多次"的典型应用(对应第 2 章的角色扮演维度)。
五、库存估值:成本口径
- 统一口径:库存金额必须按一致的计价政策计算(标准成本 / 移动加权平均 / FIFO),不能直接用每批采购价加总。
- 成本放维度:在产品维度(或独立成本维度)维护标准成本,并用SCD 管理其变化——覆盖成本会让历史快照被追认为新成本,毛利与库存金额的历史口径全部改写。
- 实际成本放事务:批次实际成本、运费、关税、价差进采购/收货事务事实表或**成本层(cost layer)**模型。
- 需要按批次估值:把批次放进快照粒度,并配套成本层表。
| 想看的口径 | 放在哪里 |
|---|---|
| 库存估值金额 | 快照事实表,按统一标准成本计算 |
| 数量 | 快照事实表,按最小库存单位计量 |
| 实际采购成本/价差 | 采购/收货事务事实表、成本层模型 |
5.1 为什么不能拿每批采购价加总
- 同一 SKU 成本不同:1 月这批 10 元/件,3 月这批 12 元/件。100 件混着两批,加出来既不是标准成本,也不是任何单一计价政策的结果——语义混乱。
- 不可比、不可对账:财务按政策估出 1050,BI 里是 1100,差 50,而且这个差不是差异,是口径不同,无法归因。
- 价差被混进存量:采购价差、运费、关税本该是事务层面的信息,搅进库存余额里就再也拆不出来。
- 趋势失真:供应商涨价、促销采购、汇率波动全压在库存金额里,分不清是"库存多了"还是"单价贵了"。
5.2 标准成本用哪种 SCD
| 类型 | 后果 |
|---|---|
| Type 1(覆盖) | 历史快照回看时按新成本重算,历史口径被追认改写,审计过不了 |
| Type 2(新增行) | 按生效日期保留版本,历史快照关联旧版本,历史口径锁死 ✅ |
口诀:数量按实际,金额按口径——标准成本进维度,实际成本进事务。
六、库存地点:DC 与门店
判断标准是“粒度与维度是否同构”,不是"地方不同就分表"。
| 方案 | 适用 | 做法 | 好处 |
|---|---|---|---|
| A. 合并 | 两边粒度/度量同构 | 一张快照 +库存地点维度(地点类型 = DC/门店/在途) | 全链路总库存一条 SQL;共用产品/日期维度;一套 ETL;调拨天然可分析 |
| B. 分开 | 维度或度量明显异构 | 各自建表,共用产品与日期一致维度 | 避免大量 NULL 与互不适用的属性(DC 要托盘/批次,门店要陈列位/零售价) |
判断顺序:先问"能不能一张表装下",装得下就合并;装不下(属性互斥、度量不同)才拆。
⚠️ 注意:合并/分开是建模决策,不是"查询时要不要一起算"。分表也能查总库存(UNION 或一致维度),只是麻烦。
无论分合,必须定"所有权规则":在途、寄售、DC→门店运输途中的货只能归发货方或收货方之一。规则不定,合并表算出的"总库存"必然虚高,而且虚高的部分永远对不平。
七、库存事务事实表:变动与流量
FactInventoryTransaction(单据行级) ├── 维度:date_key(角色扮演:单据日期 / 过账日期) ├── 维度:product_key ├── 维度:from_location_key ← 角色扮演地点(源) ├── 维度:to_location_key ← 角色扮演地点(目标) ├── 维度:transaction_type_key(收货/发货/销售出库/退货入库/ │ 调拨/盘盈盘亏/报废) ├── 维度:employee_key、reason_key ├── 退化维度:document_no(收货单号 / 调拨单号 / 盘点单号) └── 事实:qty_signed(带符号)、value_signed(带符号)三个关键词:单据行粒度、地点角色扮演(from/to)、带符号事实。
调拨的符号规则:调出地点为负、调入地点为正、全公司合计净额为 0。所以调拨不能记"单边减",而要一行同时带 from/to。
实现说明:上面写的是教材常见的
qty_signed / value_signed(带符号)。本系列的参考 DDL(inventory_model_ddl.sql)选择存正数量,再由from_location / to_location派生符号——两种实现等价,后者能自然表达调拨两端,避免一行只能承载一个符号。
与快照的关系:流量 vs 存量,互补而非替代
期初库存 + 期间净流量(入 − 出 ± 调整) = 期末库存- 这条等式是对账桥;两边对不上时,差异本身就是最有价值的信息(时间戳口径不同、倒记账、跨期过账、漏采事务类型、真实账实差异)。
- 快照不该由事务表推导:账面流水推不出实盘损耗,也补不出期初与系统切换前历史。两者各自从源系统独立采集才可信。
| 库存快照 | 库存事务 | |
|---|---|---|
| 回答 | 某时点还剩多少 | 期间动了多少、为什么 |
| 粒度 | 产品 × 地点 × 快照日 | 产品 × 地点 × 单据行 |
| 增长 | 固定节奏,可预测 | 随业务波动 |
| 用途 | 水平、周转率、库龄、库容规划 | 差异归因、单据追溯、审计、补货 |
口诀:快照看结果,事务看原因;两个都要,还要用等式互相验。
八、产品包装与单位口径(UOM)
包装规格变化(单件装 → 六件装)的应对:
- 事实表统一到最小库存单位(base UOM,如"件")——数量列里绝不能混"件"与"箱"
- 换算关系放进维度(件 → 内包装 → 箱 → 托盘,1 箱 = 6 件)
- 规格变更走 SCD Type 2——覆盖(Type 1)会让历史快照被追认为新包装,历史数量口径被静默改写
- 维度保留"产品族/基础单品"属性,让多包装归并回同一单品看总量
- 同一单品长期多包装并存 → 考虑**"包装/规格"迷你维度**
判断"算不算产品变了":
- 老包装停产、新包装替代 → 产品维度SCD Type 2
- 六件装作为独立可售 SKU 同时卖 →新增产品行,不是 SCD
口诀:换算进维度,数量归口到最小单位——包装变了走 Type 2,别覆盖。
九、库存 vs 销售:建模对比
| 对比项 | 销售建模(第 3 章) | 库存建模(本章) |
|---|---|---|
| 本质 | 流量,完全可加 | 存量,半可加 |
| 主体事实表 | 事务型 | 周期快照 |
| 粒度 | 小票行项目级 | 产品 × 地点 × 快照日 |
| 查询模式 | 期间累计 | 时点/期末 + 平均派生指标 |
| 金额口径 | 成交价,客观已实现 | 依赖成本政策,口径是人定的 |
| 自洽性 | 单表基本自洽 | 快照说结果,需配事务表说原因 |
| 数据增长 | 随业务量波动 | 线性可预测 |
一句话:销售回答"发生了多少",库存回答"现在还剩多少"——流量可加,存量半可加。
十、三类事实表在库存章的映射
| 类型 | 库存中的对应 | 记录什么 |
|---|---|---|
| 事务型 | 库存事务事实表(收货/发货/调拨/盘点) | 流量:动了多少、为什么动 |
| 周期快照 | 库存快照事实表(主角) | 存量:某时点还剩多少 |
| 累积快照 | 补货/调拨流程(下单→审批→发货→在途→收货) | 流程走到哪一步 |
对照第 3 章:零售是"事务型为主",库存是"周期快照为主"——同一套三类事实表,主战场随业务过程而变。
十一、常见误区(逐题复盘版)
这一节来自 Q1–Q10 的实际作答纠错,都是最容易踩的坑:
| # | 误区 | 正确理解 |
|---|---|---|
| 1 | “半可加 = 要先上卷到某个层级才能加” | 上卷是维度层级,与可加性无关。半可加 =除时间外都可加;跨产品/地点直接可加 |
| 2 | “不可加 = 不是所有维度都能加” | 那正是不可加的定义。半可加是恰好一个维度(时间)不可加 |
| 3 | “平均库存没有商业价值” | 平均库存是周转率的分母。正确原则:期末是原始事实,平均是派生指标——存原始、算派生 |
| 4 | “合并/分开取决于这次要不要算总库存” | 那是查询策略。判据是粒度/维度/度量是否同构 |
| 5 | “快照足够细就能变成事务表” | 快照只有状态,没有单号、类型、原因、from→to;两者是结果 vs 原因,互补不可互推 |
| 6 | “库存 = 入库 − 出库 − 调拨” | 正确是期初 + 净流量(入 − 出 ± 调整)= 期末;调拨对调出方减、调入方加 |
| 7 | “包装变更用 SCD 就行” | 必须是Type 2;Type 1 会追认改写历史口径 |
| 8 | “DC 和门店分开建更清晰” | 粒度/度量同构就合并,靠地点类型区分;异构才分表,且共用产品/日期维 |
| 9 | 忘记"所有权规则" | 在途货只能归发货方或收货方之一,否则总库存虚高且永远对不平 |
| 10 | 把快照当"更粗的事务表" | 快照是固定节奏的状态监控,不是事务的聚类;存储不是主要理由,信息内容才是 |
十二、速查:本章口诀合集
- 黄金粒度:产品 × 地点 × 每日快照;日能上卷,周不能下钻。
- 半可加:跨产品跨门店可加,跨时间不可加;要期末值,或要平均值。
- 估值:数量按实际,金额按口径——标准成本进维度(SCD Type 2),实际成本进事务。
- 地点:同构则合并,异构才分表;先定所有权规则,再谈总分库存。
- 对账:快照看结果,事务看原因;两个都要,还要用等式互相验。
- UOM:换算进维度,数量归口到最小单位;包装变了走 Type 2,别覆盖。
- 总纲:存量 vs 流量 → 周期快照 vs 事务型;半可加 → 期末/平均;结果 vs 原因 → 两张表。
十三、自测题(按四类问题组织)
建议先自己口述回答,再展开对照。答案已压缩成要点。
🔍 核心概念类
Q1:库存事实表用哪一类?为什么不能用事务型记录库存水平?用周期快照事实表。库存水平是时点状态,事务型记录的是事件/流量,回答的是"这期间动了多少"而不是"还剩多少";且事务可被漏采、时间戳口径不一,无法可信地还原库存余额。
Q2:什么是半可加事实?为什么库存跨时间不能加总?半可加 =除时间外,沿所有维度都能求和的事实。库存数量/金额跨产品、地点可加(同一时点货物互不重叠);跨时间不可加,因为每个快照是时点状态,同一件货会天天被重复计数。时间上要用期末值或平均值。
Q3:库存快照的粒度是什么?为什么日粒度打底?一行 = 一个产品 × 一个库存地点 × 一个快照日期(可选批次、状态),固定时刻取期末值。日粒度:可上卷到周/月,不可逆;能回答缺货、补货、周内波动等运营问题;数据量线性可预测;上卷到周/月时半可加度量要用期末或平均,不是 SUM。
🧩 设计思考类
Q4:快照记录哪些度量?为什么记期末而不是平均?记期末数量(最小单位)+ 期末金额(统一成本口径),可选零售金额、在途、可售/冻结;单价与比率不进事实表。期末是原始状态,平均是派生指标——存平均会焊死聚合口径,丢失原始分布、波动和缺货时点,且无法还原期末。
Q5:金额用什么成本口径?为什么必须统一?统一计价政策(标准成本 / 移动加权平均 / FIFO 三选一)。每批采购价加总得到的是口径杂糅的数:不可比、不可对账、价差混进存量、趋势失真。标准成本放产品维度 + SCD Type 2;实际批次成本、运费、关税、价差放事务表或成本层。
Q6:包装规格变化(单件装→六件装)怎么应对?① 事实数量统一到最小库存单位,绝不混件/箱;②换算关系放维度;③ 规格变更走SCD Type 2(Type 1 会改写历史口径);④ 维度保留基础单品/产品族属性;⑤ 长期多包装并存时抽成包装迷你维度。
💡 实践应用类
Q7:DC 和门店库存分开建还是合并?判据是粒度/度量是否同构:同构就合并——一张快照 + 地点维度,靠"地点类型(DC/门店/在途)"区分;异构才分表,但共用产品/日期一致维度。无论分合,必须定所有权规则,在途货只能归发货方或收货方之一。
Q8:入库/出库/调拨怎么建模?和快照什么关系?用库存事务事实表(事务型),一行 = 单据行,维度含角色扮演日期、角色扮演地点(from/to)、事务类型、员工、原因,退化维度放单号,事实带符号。与快照是流量 vs 存量,期初 + 净流量 = 期末是对账桥;快照不能由事务推导。
🤔 反思类
Q9:库存建模和销售建模相比,最核心的特殊性是什么?存量 vs 流量 → 半可加 vs 完全可加。销售跨时间可加、以事务型为主、金额是客观成交价、单表自洽;库存跨时间不可加、以周期快照为主、金额依赖成本政策、且必须配事务表对账。可加性的差异是从"存量"本质一路推下来的。
Q10:有快照了,为什么还需要事务表?两者回答不同问题,不是粗细版本:快照是结果(还剩多少,支撑水平/周转/库龄/库容),事务是原因(动了多少、为什么,支撑归因/追溯/审计/补货)。快照再细也补不出单号、类型、原因和 from→to;事务也推不出盘亏与期初。互补,用等式互相验证。
本章要点速记
- ✅ 库存的主战场是周期快照事实表:"还剩多少"是状态,不是事件
- ✅ 库存数量/金额是半可加事实:跨产品、跨地点可加,跨时间不可加;用期末值或平均值
- ✅ 粒度:一行 = 产品 × 地点 × 快照日期;日粒度打底,固定时刻拍
- ✅ 存期末不存平均:期末是原始事实,平均是派生指标
- ✅ 估值用统一成本口径(标准成本放维度 + SCD Type 2);实际批次成本进事务表/成本层
- ✅ 包装规格变更走SCD Type 2;数量统一到最小库存单位,换算关系放维度
- ✅ DC 与门店:同构则合并(靠地点类型区分),异构才分表,但共用产品/日期一致维度
- ✅ 库存事务表记流量,快照记存量;
期初 + 净流量 = 期末是对账桥 - ✅ 快照不该由事务推导——它承载账实差异与损耗,是"被测量"的
- ✅ 与销售的终极差别:半可加性——第 2 章埋的伏笔,在这一章兑现
一句话收尾:库存建模的本质,是把"某一刻还剩多少"这个状态,用固定节奏、最小单位、统一口径、可对账的方式记录下来,并和"它是怎么变来的"分开存放、互相验证。