想象你经营一家连锁餐饮集团,每天几十家门店会送来五花八门的食材:带泥的土豆、未宰杀的鱼、装在不同箱盒里的订单小票……你不会让厨师直接拿这些原料做菜,更不能让每位客人自己到仓库翻找。聪明的做法是建一个中央厨房:收货区原样存放食材,粗加工间洗切分装,预制菜间按标准菜谱做成半成品,前厅再根据顾客点单快速出餐。数据仓库的分层,就是给企业数据搭建这样一条“中央厨房流水线”。ODS 是收货区,DWD 是粗加工间,DWS 是预制菜间,ADS 是前厅出品台。理解了这套流水线,你就理解了数据从原始、明细、汇总到应用的流转逻辑。

岗位关联

对数据工程师、数仓架构师和 AI 从业者而言,掌握分层架构能让你设计出可维护、可复用、口径统一的数据底座,这是训练模型、做分析报表之前最重要的地基工程。

目录
  1. 可视化演示
  2. 分层核心理念
  3. ODS原始层
  4. DWD明细层
  5. DWS汇总层
  6. ADS应用层
  7. ETL流转
  8. 命名约定
  9. 建设原则
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

通过ODS-DWD-DWS-ADS四层架构流转、ETL数据加工流水线、命名规范树形结构三个模式,展示数仓分层设计的核心方法论。

本节配有交互动效(进入视口后自动加载,约 3KB)。

ODS-DWD-DWS-ADS四层架构 ODS原始数据层保持原始结构DWD明细层清洗标准化DWS汇总层聚合统计ADS应用层面向业务 分层设计实现数据复用降低重复计算每层职责清晰可维护可追溯 Source → ODS → DWD → DWS → ADS → Application 数据分层是数仓规范化的核心实现数据从原始到应用清晰流转

分层核心理念

一家连锁餐饮集团的中央厨房,绝不会让带泥的土豆直接进炒锅。老板会把整条链路拆成四个站:收货区只管收货,粗加工间负责洗切,预制菜间按菜谱做大锅半成品,前厅再按订单装盘。每站只做一件事,出了问题也只需追责到一站。数据仓库要解决的核心问题,本质上和这个一模一样:企业数据从业务系统里出来,形态芜杂、口径不一,若直接喂给报表和算法,必然一团乱麻。

所以数仓把数据切成分层:ODS 是原始数据的收货区,DWD 是清洗转换后的标准明细,DWS 是按主题汇总的公共指标层,ADS 是面向业务应用的结果层。四层不是四张表,而是一套职责边界。每一层都有明确的输入、输出和质量标准,上游产出什么,下游才能消费什么。

分层带来三个直接收益:口径统一,不管谁做报表都从 DWS 取同一个 GMV;影响隔离,源系统字段变更最多影响 ODS 接入,不会震到 ADS;复用降本,公共汇总只算一次,多份应用共享。代价是多了一层冗余存储和调度,所以也叫“空间换时间”。

就像中央厨房每站都有明确的进出货标准,数据层也通过清晰边界让团队能独立协作。你不需要让每个人都懂全链路,但只要守住层与层之间的接口,流水线就能稳定运转。

ODS→清洗转换DWD→主题汇总DWS→按需应用ADS\text{ODS} \xrightarrow{\text{清洗转换}} \text{DWD} \xrightarrow{\text{主题汇总}} \text{DWS} \xrightarrow{\text{按需应用}} \text{ADS}
  1. ODS 到 DWD数据从原始层流入明细层,核心动作是清洗和标准化。
    ODS→DWD\text{ODS} \to \text{DWD}
  2. DWD 到 DWS明细数据按主题聚合成公共指标,核心动作是汇总。
    DWD→DWS\text{DWD} \to \text{DWS}
  3. DWS 到 ADS汇总结果按业务应用取出,核心动作是面向需求加工。
    DWS→ADS\text{DWS} \to \text{ADS}
  4. 完整链路四个节点串起来就是数仓的“中央厨房流水线”。
    ODS→DWD→DWS→ADS\text{ODS} \to \text{DWD} \to \text{DWS} \to \text{ADS}

ODS原始层

收货区的工作原则是:先把货收下,别急着改。供应商送来的土豆带着泥,鱼还带着鳞,收货员只清点、贴标签、按到货日期放上货架。如果收货时就把泥洗掉、把鱼切好,万一供应商想对账,你反而说不清原始货长什么样。

ODS 层对应这种“原件仓库”。它从业务库、日志、接口文件中抽取数据,按同步策略落地到数仓。ODS 不负责数据质量,不 join 多张表,更不聚合;它只忠实记录源系统的每一次快照或增量。原始字段叫什么就叫什么,原始值是什么就是什么,哪怕看起来是脏数据也要先留着。

ODS 设计要点包括三块:一是接入方式,小表常用全量快照,大表按日期或主键做增量;二是分区策略,几乎都按 dt 天分区,便于回溯;三是生命周期,原始日志通常保留 30 到 180 天,按成本和合规要求治理。因为 ODS 直接接触源,任何源表结构变化都要在这里先做兼容。

菜收下不等于能直接上桌,但收货区必须原样留底;ODS 也一样,它是整个数仓最接近真相的“证据库”。后续任何清洗逻辑出了问题,都能回到这一层核对原始面目。

ODS=⋃i=1nSourcei\text{ODS} = \bigcup_{i=1}^{n} \text{Source}_i
  1. 并集符号 $\bigcup$表示 ODS 由多个来源表汇集而成,不是单一表。
    ⋃i=1n\bigcup_{i=1}^{n}
  2. 源系统 $\text{Source}_i$第 $i$ 个业务源表,比如订单表、用户表、日志表。
    Sourcei\text{Source}_i
  3. 接入个数 $n$源系统的数量会随着业务扩张而增加。
    nn

DWD明细层

粗加工间里,净菜师傅接手带泥土豆,先冲洗、去皮、切块,再按统一规格装进保鲜盒。同样一颗土豆,在中餐和西餐厨房里可以有不同切法,但中央厨房必须提前定好标准:块就是块,丝就是丝,标签上写清楚。

DWD 是数仓里的净菜间。它把 ODS 里杂乱的数据清洗、转换、标准化,结果仍然是明细粒度。所谓明细粒度,就是一行对应一件真实业务事实:一条订单、一次点击、一个用户注册行为。DWD 不做汇总,它保留的是“每一件事实发生了没有、涉及谁、值是多少”。

DWD 常见动作包括:清除空值和脏数据、统一日期与金额格式、把源系统里的代码翻译成统一枚举、做维度退化。维度退化是把常用维度属性直接落进事实表,减少下游 join。比如订单表里直接带上商品类目和门店名称,查询时就不用每次关联维度表。

只有净菜标准统一,预制菜间才敢大批量生产;只有 DWD 标准统一,上层汇总才能放心复用。DWD 是整个数仓里最值得反复打磨的一层,因为它决定了上游一次加工的质量,也决定了千万次查询的体验。

rDWD=⟨dim_key, attributes, measures⟩r_{\text{DWD}} = \langle \text{dim\_key},\ \text{attributes},\ \text{measures} \rangle
  1. 维度外键 dim_key指向统一维表的钥匙,例如 user_id、sku_id。
    dim_key\text{dim\_key}
  2. 属性 attributes随行保存的冗余维度属性,例如商品类目、门店名称。
    attributes\text{attributes}
  3. 度量 measures可累加的数值事实,例如下单金额、件数。
    measures\text{measures}

DWS汇总层

预制菜间不追求菜品多,而追求“大锅出精品”。鱼香肉丝的酱料一次熬 100 人份,分装成料包;门店出餐时不再各自调酱,味道自然一致。这就是公共加工的意义。

DWS 层按主题对 DWD 明细做汇总,生成公共宽表和指标。主题可以是用户、商品、交易、物流等。比如 dws_trade_gmv_d 就是按天、按门店、按商品粒度算出交易总额。它的核心不是展示某一次订单,而是沉淀大家都会用的统计口径。

设计 DWS 时要选对粒度:太小则复用价值低,太大则细节丢失。常用做法是“日加一个业务维度”,例如日门店商品汇总。指标在这个层统一命名、统一计算逻辑,下游 ADS 只取数不重算,这样 GMV 不会出现八种答案。

预制菜一次做一大批,省的是每家门店重复炒制的时间;DWS 把公共指标算一次,省的是每个报表重复扫描明细的成本。这就是数仓“复用”二字最实在的体现。

Mday=∑i∈FdayviM_{\text{day}} = \sum_{i \in F_{\text{day}}} v_i
  1. $F_{\text{day}}$某一天的事实集合,也就是 DWD 中当天的明细行。
    FdayF_{\text{day}}
  2. 求和 $\sum$把每行事实的度量值做累加。
    ∑i∈Fday\sum_{i \in F_{\text{day}}}
  3. $v_i$第 $i$ 行事实的贡献值,例如订单金额或下单件数。
    viv_i

ADS应用层

前厅出品台的工作是“看着菜单出菜”。客人点鱼香肉丝,后厨从预制菜间取料包,热锅翻炒、装盘、撒葱花,再端到桌上。同一个预制菜,可以根据门店定位做微调,但不能让每个客人都进后厨自己找料。

ADS 就是数仓的出品台。它面向具体报表、BI、数据产品和接口,按需从 DWS 取汇总指标,必要时再补 DWD 明细,加工成最终的宽表或数据集市。比如 ads_user_purchase_feature 直接供算法取数,里面已经按用户聚合好近 30 天消费特征。

ADS 的特点是“快而专”:可以为了特定查询做一定的冗余,但必须克制。ADS 不该重复建设通用指标,也不该把明细无限堆进来。每张 ADS 表都应该能说清它服务什么场景、由谁维护、数据从哪一层来。

出品台不负责研究菜谱,只负责把已验证的半成品组合成客人要的那盘菜;ADS 不做公共口径研发,只做面向需求的高效输出。层与层之间的分工,在这一站画上句号。

Qreport=π指标(σ条件(DWS))Q_{\text{report}} = \pi_{\text{指标}}\left( \sigma_{\text{条件}}\left( \text{DWS} \right) \right)
  1. 选择 $\sigma$按业务条件过滤,例如只看华东区或近 30 天。
    σ条件(DWS)\sigma_{\text{条件}}(\text{DWS})
  2. 投影 $\pi$只保留报表需要的指标列,减少输出体积。
    π指标(⋅)\pi_{\text{指标}}(\cdot)
  3. $Q_{\text{report}}$最终给到报表或接口的结果集。
    QreportQ_{\text{report}}

ETL流转

中央厨房里最怕的不是做菜难,而是备餐顺序乱。炖汤要提前 3 小时开火,切配要在开餐前 1 小时完成;传菜梯一次只能上一层,高峰还要排队。没有动线规则,后厨就会撞车。

数仓的 ETL 就是数据动线。ETL 即抽取、转换、加载,实际落地靠调度系统编排无数个任务:ODS 同步任务、DWD 清洗任务、DWS 汇总任务、ADS 生成任务。任务之间必须声明依赖,下游在上游成功后再跑。

流转规范还包含三件事:数据质量监控,在每层入口检查空值率、主键唯一性、行数波动;增量策略,大表避免每天全量重刷;血缘追踪,记录每张表的上游任务和 SQL 来源,出了问题能顺着血缘往回查。

传菜动线理顺了,出餐才稳定;ETL 的依赖和血缘理顺了,数仓数据才能稳定可信地流向每一张报表。没有动线纪律,再好的分层也会被临时任务搅乱。

G=(V,E)G = (V, E)
  1. 顶点集 $V$表示所有 ETL 任务节点,比如 dwd_order_clean。
    V={Task1,Task2,…,Taskn}V = \{\text{Task}_1, \text{Task}_2, \dots, \text{Task}_n\}
  2. 边集 $E$表示任务之间的依赖关系,$u \to v$ 表示 $v$ 依赖 $u$ 完成。
    E={(u,v)∣u 完成后 v 才能启动}E = \{(u, v) \mid u \text{ 完成后 } v \text{ 才能启动}\}
  3. 图 $G$整个调度 DAG,不允许出现循环依赖。
    GG

命名约定

在中央厨房,标签是纪律。一个冷藏货架上如果只写“食材”,没人知道里面是土豆还是鱼;规范的标签应写成“A区-冷藏-净菜-土豆丝-20250101”。这样任何人都能在 10 秒内找到东西,也知道它属于哪个环节。

数仓表名同样需要“标签格式”。业界通用规则是:层级前缀加主题域加业务过程加周期粒度。例如 dws_trade_gmv_d:dws 表示汇总层,trade 是交易主题,gmv 是业务过程,d 表示日粒度。看到表名就能知道它从哪层来、在讲什么业务、按什么周期更新。

命名约定还包括分区规范,绝大多数表用 dt='YYYY-MM-DD' 分区;维度表用 dim 前缀,事实表用 dwd/dws 前缀,临时表用 tmp 前缀。统一命名让数据地图可读、权限可配、血缘可查。

标签写得清,中央厨房才能高效运转;表名符合规范,数据开发才能不靠口头解释定位一张表。命名不是小事,它是数仓长期可维护性的第一道防线。

TableName=layer_domain_business_granularity\text{TableName} = \text{layer}\_\text{domain}\_\text{business}\_\text{granularity}
  1. 层级 layer表所在的分层,如 ods、dwd、dws、ads。
    layer∈{ods,dwd,dws,ads}\text{layer} \in \{\text{ods}, \text{dwd}, \text{dws}, \text{ads}\}
  2. 主题域 domain业务域,如交易 trade、用户 user、商品 sku。
    domain\text{domain}
  3. 业务过程 business具体动作,如下单 order、支付 pay、退款 refund。
    business\text{business}
  4. 粒度 granularity时间或维度粒度,如 d 表示天、m 表示月。
    granularity\text{granularity}

建设原则

好的中央厨房,不是设备堆得越多越好,而是每个区域高内聚、低耦合:收货区不要放炒锅,前厅不要设洗菜池。区域的边界一旦模糊,动线就会交叉,效率反而下降。

数仓建设原则可以总结为四句:高内聚低耦合,同一主题的表在 DWS 内聚,不同层之间只通过标准接口流转;分层治理,每层有独立的负责人和验收标准;稳定可控,公共层变更要经过评审和回归测试;持续演进,新需求先复用已有模型,而不是立刻新增一张表。

成本控制是原则里的隐形标尺。DWD 保留明细是必要的存储成本,DWS 冗余宽表是换取查询速度的投资。判断一个新层或新表是否值得,就看它能否提升复用率、降低下游重复计算。一个简单的度量是复用率等于被下游引用的模型数除以总模型数。

中央厨房的管理者每周都会看哪些预制菜被点得多,数仓团队也要定期追踪被引用次数,把重复的“小灶”合并回公共层,让流水线持续变好。建设原则不是写在PPT里的口号,而是每次建模前都要问自己的问题。

ReuseRate=NdownstreamNtotal\text{ReuseRate} = \frac{N_{\text{downstream}}}{N_{\text{total}}}
  1. 被复用模型数 $N_{\text{downstream}}$被至少一个下游任务引用的公共模型数量。
    NdownstreamN_{\text{downstream}}
  2. 总模型数 $N_{\text{total}}$数仓中所有可复用层模型的总量。
    NtotalN_{\text{total}}
  3. 复用率 ReuseRate越高说明公共建设越有效,越低则要警惕重复造表。
    ReuseRate\text{ReuseRate}

AI 应用场景

离线数仓统一指标口径

阶段:数据集成与治理

解决的问题:未分层时,不同部门各自从源表取数,GMV、活跃用户等指标经常口径不一致。分层后,ODS 负责原样接入,DWD 负责清洗,DWS 统一沉淀公共指标,ADS 只面向报表取数,所有消费方共享同一份“预制菜”。

具体案例:某电商平台原本 50 张报表算出 8 个 GMV,业务会议上对不上数。重建四层数仓后,DWS 只有一张 dws_trade_gmv_d,所有报表和 BI 看板都引用这张表,口径从 8 个收敛为 1 个。

机器学习特征工程

阶段:特征工程与数据准备

解决的问题:算法团队要训练用户购买力模型,需要“近 30 天消费金额”等特征。如果每次从原始日志临时计算,慢且不稳定,训练和上线特征还可能不一致。通过 DWD 清洗行为明细、DWS 按用户汇总交易指标,再生成 ADS 特征宽表,可以保证特征口径一致、可复用。

具体案例:某推荐系统团队用 DWS 用户交易宽表叠加 DWD 浏览日志,生成特征表 ads_user_purchase_feature,供模型离线训练和线上打分共同使用,特征延迟从小时级降到分钟级。

Python 演示

python50 行
# -*- coding: utf-8 -*-from collections import defaultdict  class LayeredWarehouse:    def __init__(self):        self.ods = []        self.dwd = []        self.dws = []        self.ads = []     def load_ods(self, rows):        self.ods.extend(rows)     def clean_to_dwd(self):        self.dwd = []        for row in self.ods:            if row.get("amount") is None:                continue            clean = dict(row)            clean["user_id"] = str(clean["user_id"]).strip()            clean["amount"] = float(clean["amount"])            self.dwd.append(clean)     def aggregate_to_dws(self):        grouped = defaultdict(float)        for row in self.dwd:            grouped[row["date"]] += row["amount"]        self.dws = [{"date": d, "gmv": v} for d, v in sorted(grouped.items())]     def build_ads(self):        self.ads = [r for r in self.dws if r["gmv"] >= 1000]     def run(self):        self.clean_to_dwd()        self.aggregate_to_dws()        self.build_ads()  if __name__ == "__main__":    wh = LayeredWarehouse()    wh.load_ods([        {"date": "2025-01-01", "user_id": " u1 ", "amount": 120.0},        {"date": "2025-01-01", "user_id": "u2", "amount": None},        {"date": "2025-01-02", "user_id": "u3", "amount": 1200.0},    ])    wh.run()    print(wh.dwd)    print(wh.dws)    print(wh.ads)
目的:用一段迷你代码模拟四层数仓:ODS 原样入库、DWD 清洗转换、DWS 按日汇总 GMV、ADS 输出高价值报表。
  1. self.ods.extend(rows)把源数据整包放进 ODS,不挑不拣,对应收货区原样收菜。
  2. if row.get("amount") is None: continueDWD 清洗时丢弃空值,而不是在 ODS 里动手脚。
  3. clean["amount"] = float(clean["amount"])统一金额类型,把字符串或整数转成可计算的浮点数。
  4. grouped[row["date"]] += row["amount"]按日期累加,正是 DWS 层最常见的汇总动作。
  5. self.ads = [r for r in self.dws if r["gmv"] >= 1000]ADS 按报表阈值过滤,只输出业务需要的结果。
  6. wh.run()把 DWD、DWS、ADS 串成一条可重复执行的流水线。

常见误区

误区ODS 就是放数据的临时桶,我顺手把空值和格式都改掉,省得 DWD 再处理一遍。

正解ODS 的目标是“原样落地、可追溯”,不是清洗。若在 ODS 改数据,源系统问题会被抹掉,排查血缘时看不到真实原貌。清洗应放在 DWD,因为 DWD 才负责“净菜”。

为何常见:许多人把 ODS 当作可随意操作的工作区,忽略了它作为原始证据的定位。

误区DWD 也是明细,ODS 也是明细,两层不是重复建设吗?

正解虽然都是明细粒度,但 DWD 是“标准化净菜”:统一字段命名、统一枚举值、消除不一致维度;ODS 是“带泥食材”。二者服务于不同阶段,DWD 让公共加工在下游可以复用。

为何常见:只看到“行数没变”,没有看到字段标准和质量发生的变化。

误区DWS 是给报表用的,我就把明细和汇总全放一张大表,查啥都有。

正解DWS 的核心是公共汇总指标和宽表,不是明细中转站。如果明细进 DWS,表会膨胀、口径难维护,下游还容易绕过 DWD 直接用明细,破坏层次。明细留给 DWD,DWS 只沉淀高频复用的汇总指标。

为何常见:为了暂时查询快,忽略了分层职责。

误区ADS 是应用层,指标越多越显得数仓完善。

正解ADS 面向具体业务场景,按需构建。全量堆指标会造成口径重复、资源浪费,且新需求难以维护。ADS 只放真正被报表或产品使用的指标,像前厅只做菜单上的菜,不做满汉全席。

为何常见:把“完善”理解为“多”,而不是“准”和“快”。

误区我看到别人有 7 层、9 层,我们只有 4 层,是不是太落后了?

正解分层是空间换时间,层数过多会增加调度延迟、存储成本和维护复杂度。ODS、DWD、DWS、ADS 是业界验证过的平衡结构。新增层必须能带来明确的口径或复用收益,否则就是过度设计。

为何常见:盲目追求架构复杂度,忽略成本和收益。

面试题

Q1. 数仓四层架构中,哪个层级负责原样保存来自业务系统的数据?

单选题 · easy

  • A.ODS 原始层← 正确答案
  • B.DWD 明细层
  • C.DWS 汇总层
  • D.ADS 应用层

答案:A。ODS 就像中央厨房收货区,先把供应商送来的食材原样放好,不急着清洗。DWD 才开始洗切,DWS 做预制菜,ADS 负责最终出餐。

Q2. DWD 层相对 ODS 层,最主要的区别是什么?

单选题 · easy

  • A.对数据进行清洗与标准化← 正确答案
  • B.只修改一下表的名字
  • C.把所有历史数据都删除
  • D.直接输出最终报表指标

答案:A。DWD 是粗加工间,负责去空值、统一类型、对齐维度;它不直接出报表,也不删除历史。C 是破坏数据,D 是 ADS 的事。

Q3. DWS 层通常存放哪类数据?

单选题 · easy

  • A.主题化的汇总宽表← 正确答案
  • B.原始接口日志文件
  • C.前端页面静态资源
  • D.数据库临时查询缓存

答案:A。DWS 是预制菜间,把 DWD 的明细按主题聚合成宽表。原始日志归 ODS,静态资源不属于数仓。

Q4. 表名 dws_trade_gmv_d 中,最后的 d 通常代表什么?

单选题 · mid

  • A.按天分区← 正确答案
  • B.数据库名
  • C.维度表
  • D.明细层

答案:A。命名规范常用 d 表示 day 日分区,类似厨房标签上的日期。若是维度表一般写 dim,明细层一般由前缀 dwd 表达。

Q5. 设计 ODS 层时,下列哪项做法是正确的?

单选题 · mid

  • A.保留源系统的原始数据结构← 正确答案
  • B.将空值全部替换为 0
  • C.把多张表直接合并成宽表
  • D.过滤掉所有异常订单

答案:A。ODS 是原件仓库,应保留源系统原貌。空值替换、合并宽表、过滤异常都属于 DWD 清洗或 DWS 汇总的职责,提前做会破坏证据链。

Q6. 数仓分层带来的价值有哪些?

多选题 · mid

  • A.统一指标口径← 正确答案
  • B.降低下游开发成本← 正确答案
  • C.让每层职责更清晰← 正确答案
  • D.让源库结构不再变更

答案:ABC。分层用空间换时间,把公共加工沉淀到公共层,下游不用重复造轮子;也便于血缘追查。但它不能阻止源业务系统改变表结构,只能隔离影响。

Q7. 关于 DWD 层的建设,正确的描述有哪些?

多选题 · mid

  • A.清洗空值和错误格式← 正确答案
  • B.统一枚举值与编码← 正确答案
  • C.保留明细事实粒度← 正确答案
  • D.按天汇总公共指标

答案:ABC。DWD 是净菜间,清洗、标准化、保持明细粒度;按天汇总 GMV 属于 DWS 的聚合动作,不应混入 DWD。

Q8. 数据血缘追踪主要依赖以下哪类信息?

单选题 · hard

  • A.调度依赖和任务日志← 正确答案
  • B.报表图表的颜色配置
  • C.数据库用户名的长短
  • D.服务器 CPU 序列号

答案:A。血缘需要知道每张表由哪些上游任务产出、又被哪些下游任务消费。这些信息来自调度系统、ETL 日志和 SQL 解析,和图表配色、硬件信息无关。

Q9. 在分层数仓中,ADS 层可以直接被哪些对象消费?

多选题 · hard

  • A.报表和 BI 工具← 正确答案
  • B.业务分析师查询← 正确答案
  • C.下游数据接口← 正确答案
  • D.源业务 OLTP 库

答案:ABC。ADS 是前厅出品台,面向报表、分析和接口等消费端。源业务 OLTP 库是上游,不是 ADS 的消费者,ADS 也不应该反向影响业务系统。

Q10. 数据仓库用“空间换时间”来设计分层,以下理解最准确的是?

单选题 · extreme

  • A.用冗余存储换取查询与分析提速← 正确答案
  • B.用更多计算资源换更少磁盘
  • C.用更乱的模型换更快的开发
  • D.用删除明细换更小的空间

答案:A。DWS 和 ADS 的宽表会冗余存储部分数据,但公共指标一次算好,下游不用重复扫描明细,查询更快。C 和 D 违背分层原则,B 方向说反了。