规范化是事务型数据库的标准做法。它通过将数据组织成独立的相关表来最大限度地减少冗余。这种结构保证了当客户更新地址或产品价格变化时,修改只在一个地方发生。这种做法维护了数据完整性并优化了写入操作。然而,当目标转向读取和汇总大量数据时,这种分散的数据布局会带来显著的性能开销。在分析架构中,我们常采用非规范化来降低查询的复杂度和执行时间。非规范化是指通过将多个规范化表的数据合并到一个表或更少的表中,有意识地在数据库模式中引入冗余。这个过程以写入效率和存储空间换取读取性能。连接操作的高昂开销非规范化的主要原因是消除连接操作。在一个规范化模式(通常是第三范式或3NF)中,一个简单的查询,例如报告“按产品类别划分的总销售额”,可能需要连接四个或五个表:Order_Items(订单项)、Orders(订单)、Products(产品)、Categories(类别),可能还有Customers(客户)。当数据库引擎执行连接操作时,它必须根据共享键匹配一个表中的行与另一个表中的行。随着数据集从数百万行增长到数十亿行,这种操作在计算上变得昂贵。在分布式数据仓库中,连接大型表通常需要在网络中“混洗”数据,以便将匹配的键对齐到同一个计算节点上。这种网络传输比读取本地数据慢得多。通过非规范化模式,我们在数据加载过程(ETL/ELT)中预先连接这些属性。生成的表包含所有必要的描述性属性以及正在分析的指标,它们都在同一行中。digraph G { rankdir=LR; node [shape=box, style="filled, rounded", fontname="Arial", fontsize=10, margin=0.2]; edge [penwidth=1.5, color="#868e96", arrowsize=0.8]; subgraph cluster_normalized { label="规范化 (3NF)"; style=dashed; color="#adb5bd"; fontname="Arial"; node [fillcolor="#e9ecef", color="#adb5bd"]; Orders [label="订单\n(order_id, date)"]; Items [label="订单项\n(item_id, qty)"]; Products [label="产品\n(prod_id, name)"]; Cats [label="类别\n(cat_id, type)"]; Orders -> Items; Products -> Items; Cats -> Products; } arrow [shape=none, label="非规范化过程", fontsize=11, fontcolor="#495057", width=1.5]; subgraph cluster_denormalized { label="非规范化 (扁平)"; style=dashed; color="#adb5bd"; fontname="Arial"; node [fillcolor="#d0bfff", color="#7950f2"]; Sales [label="销售_扁平\n(order_id, date, qty,\nprod_name, cat_type)", width=2.5]; } Cats -> arrow [style=invis]; arrow -> Sales [style=invis]; }比较需要多次连接的规范化结构与属性已整合的非规范化结构。存储与计算的权衡过去,存储成本高昂,使得消除重复文本字符串(例如将“电子产品”作为类别名称存储一百万次)成为一项主要的经济需求。如今,存储相对便宜,而计算资源(CPU和内存)和时间是主要的成本因素。考虑查询执行的数学原理。如果我们对事实表 $F$ 和维度表 $D$ 执行连接操作,其复杂性通常与表的大小和所用的连接算法(例如哈希连接或排序合并连接)成比例。如果我们把访问一行的成本记为 $C_{读取}$,连接逻辑的成本记为 $C_{连接}$,那么规范化查询的成本近似为:$$Cost_{规范化} \approx |F| \cdot C_{读取} + |D| \cdot C_{读取} + (|F| + |D|) \cdot C_{连接}$$在一个完全非规范化表 $T$ 中,连接成本为零。公式简化为:$$Cost_{非规范化} \approx |T| \cdot C_{读取}$$尽管由于文本冗余,$|T|$(非规范化表的大小)在字节方面更大,但现代列式压缩技术减轻了这种膨胀。列式存储使用游程编码(RLE)和字典编码等编码方案。如果Category(类别)列重复“电子产品”这个值10,000次,数据库会存储该值一次并记录重复次数。因此,现代数据仓库中的非规范化很少会带来像传统行式系统那样巨大的存储开销。数据完整性管理在操作型系统中,反对非规范化的最主要理由是更新异常的风险。如果产品名称发生变化,必须在所有出现该产品的行中进行更新。如果更新中途失败,数据库将进入不一致状态。在分析架构中,我们以不同的方式处理这种风险。分析系统通常为最终用户设计为“只读”。数据通过数据工程师控制的批处理或流式管道写入。当维度属性发生变化(例如产品重命名)时,工程管道处理更新逻辑。我们接受在ETL管道中管理更新的复杂性,以期为最终用户提供更简单、更快的查询。性能影响分析当观察查询延迟时,非规范化的影响变得明显。以前需要数据库引擎为连接操作构建哈希表的查询,现在可以作为简单的扫描和聚合操作执行。这对于商业智能(BI)工具中的“切片和切块”数据尤其有效,用户在过滤仪表板时期待亚秒级响应。{ "layout": { "title": "查询延迟:规范化 vs. 非规范化", "font": {"family": "Arial", "size": 12, "color": "#495057"}, "height": 400, "xaxis": { "title": "数据集大小 (行数)", "gridcolor": "#e9ecef", "zerolinecolor": "#dee2e6" }, "yaxis": { "title": "执行时间 (秒)", "gridcolor": "#e9ecef", "zerolinecolor": "#dee2e6" }, "plot_bgcolor": "white", "margin": {"l": 60, "r": 30, "t": 50, "b": 50}, "legend": {"x": 0.05, "y": 1} }, "data": [ { "x": ["1M", "10M", "100M", "500M"], "y": [0.5, 4.2, 45.0, 210.0], "type": "scatter", "mode": "lines+markers", "name": "规范化 (5次连接)", "line": {"color": "#fa5252", "width": 3}, "marker": {"size": 8} }, { "x": ["1M", "10M", "100M", "500M"], "y": [0.2, 0.8, 5.5, 22.0], "type": "scatter", "mode": "lines+markers", "name": "非规范化 (扁平)", "line": {"color": "#228be6", "width": 3}, "marker": {"size": 8} } ] }随着数据集大小的增加,需要连接操作的规范化模式与扁平非规范化模式之间的性能差距显著扩大。非规范化的程度非规范化并非非此即彼的选择。存在与不同建模策略相符的实现程度:星型模式: 这是一种部分非规范化。我们保持中心事实表(事务)规范化,但将其周围的维度表非规范化。例如,一个产品维度可能将类别和子类别表合并到自身。这减少了连接的数量,同时保持了庞大事实表的精简。单一大表 (OBT): 这是一种极端非规范化。分析所需的所有属性都被标记到事务记录上。这会生成一个包含可能数百列的单一庞大表。在现代云数据仓库(如BigQuery或Snowflake)中,这种做法越来越普遍,因为存储与计算分离且扫描高度优化。这些模式的选择取决于更新频率以及业务用户的特定查询模式。我们将在即将到来的维度建模核心章节中阐述星型模式设计的具体内容。