维度建模工具箱 提纲与总结
这里写自定义目录标题
- 基本概念
- 事实表和维度表
- BI(Business Intelligence) 产品
- 事实表
- 事实表的粒度
- 事实表的种类
- 维度表建模技术
- 基本原则
- 避免用自然键作为维度表的主键,而要使用类似自增的整数键
- 避免过度规范化
- 避免变成形同事实表的维度表
- SCD(Slowly Changed Dimension)建模
- 其它常见规则
- 结语
这是一本数据仓库、维度建模领域的经典著作,但是也让我觉得枯燥至极。我好久没看到这么枯燥的书了——尤其是这蹩脚的翻译,为阅读增添了不少难度。这本书枯燥的原因(除了垃圾的翻译之外),可能是它太像一本工具书了,前十章都在用一个“尽量简单”的建模例子,引申出建模所要使用的一些技术。然而一般来说这种例子都非常枯燥,不知所云。其次是作者基本没有介绍书中出现的术语,比如“无事实的事实表”,“自然键”等,因此看到这些术语即觉得拗口,也很难快速吸收文字里的知识。如今已经读完这本书一月有余,希望用脑子里还剩下的东西来写一篇博文,说说这本书带给了我什么知识,必要的时候辅以工作中的实际例子来讲解一些术语。这篇文章先会进行背景介绍和基础术语介绍,然后讲解书中提到的基本建模方法。
基本概念
事实表和维度表
数据仓库中的表分为事实表和维度表。事实表一般存储了一系列事件,通常带有一些用以分析的度量(比如金额等数字),而维度表一般存储着一些相互关联的属性。举个最简单的例子,一个订单表是事实表,每一行存储着一次购物行为。而一个订单通常和以下这些“维度”都有关系,比如用户信息,比如物流信息,比如时间信息。
我们知道大数据领域,一般不太遵循SQL那一套规范化,也就是允许行与行之间有很多重复的字段。那为什么数仓里不直接把各种维度直接展开,全放到事实表里,而是要费劲地整一套维度建模理论?
首先,这里讨论的数仓并不只是数据的仓库,而是通常有一些实时查询的需求,下游可以直接从数仓的数据来构建BI报表。也就是整本书讨论的是可以支持BI下游的数仓,而不是一些基于HDFS之类的数仓。有了这一条件,数仓就有性能要求,因此抽取维度表的好处就有:
- 降低事实表的列数量,从而大大降低事实表的大小
- BI软件通常可以较好地执行一级JOIN,尤其是JOIN KEY为int时。也就是说正常情况下,JOIN维度表的性能较好
- 提高列值的标准化程度,在列值转化为维度键的时候会先看维度表中是否已有类似的值,从而避免创建相似但类似的维度(比方说大小写不一样等)
通过上面的一大段阐述,就是为了说明维度建模是很重要、很实用的技术。只有技术有其实用性,接下来才有必要讨论如何更好地进行建模。
BI(Business Intelligence) 产品
经典的BI产品有微软的PowerBI等。BI产品可以方便地搭建动态的可视化报表。比如我们都用Excel画过柱状图、饼图等,这些就是可视化的图形。把这些图形放在一个页面里,可以叫它一个报表。比方说某个商店的报表里可以包含:
- 最近卖出最多的十件商品
- 卖的最好的十件新品
- 退货率最高的十件商品
这样,管理者可以一目了然他/她最关心的一些指标,方便他做出决策。
那什么叫动态报表呢?从我使用BI的经验来看,动态主要体现在两个方面
- BI可以定时从数据库里获取最新的数据,从而自动更新报表展示的数值和统计图等
- BI支持实时聚合计算,比方说一个大公司的产品会在多个区域进行售卖,管理者可能希望分析四川和浙江畅销商品的差别;产品也分为入门级和高端产品,管理者希望分别看到入门级和高端产品中哪些产品卖的比较好。如此繁多的分析需求,人工一个一个做出报表是费时费力的。而BI产品可以自动地根据某些维度过滤或聚合数据,得到用户想要的答案。这样用户分析的自由度也大大提升了,只要是原始数据里有的维度,都可以进行自助的分类分析。那原始数据里没有的维度该怎么办?让上游提供呗:)
事实表
事实表的粒度
介绍过事实表,和数仓在BI侧的应用,接下来可以讲解事实表的粒度设计。粒度在本书中被通篇强调,因为粒度决定了下游可以进行分析的精细程度。
比方说我们有一个原始订单表,记录了用户的每一个订单,那么订单表可以有两种设计方式,这两种设计方式的粒度不同:
- 以订单作为粒度,好比我们在淘宝购物车里一次性买了好几件商品,那么这一次下单,只会在订单表里产生一行事件
- 以每个订单的每种商品作为粒度,也就是假如一个订单买了商品1和商品2,那事实表里就会有两条记录
这两种设计的最大区别是什么?从下游应用(下游应用包含BI场景,或者其它分析场景)来看,如果下游希望进行商品维度的分析,那么只有第2种方式能满足。那你可能会说,我把每个订单购买的商品信息存下来不就行了吗?这样会增加分析的复杂程度,毕竟订单和商品是一对多的关系,最终需要一个数组或者更复杂的结构(想想我们不仅关心商品种类,还关心商品购买的数量)来存储。作者虽然不建议在数仓维度建模时进行规范化,但是作者觉得第一范式(也就是每个列不要存储复杂结构如数组、对象等)的底线还是要守住的。
OK,那你可能还会问,如果我要以订单作为分析对象该怎么办?有的分析只能以订单为粒度进行,比方说满减优惠。这时可以给商品订单加上订单维度,用以保存订单的相关信息,比如订单总金额,订单收货地址,订单优惠等。然后订单维度和商品订单事实表以外键关联。
不过一切设计在没有说清楚场景的时候,都很难比较。刚刚的分析是假定需要有很多基于商品的分析,假如情况并非如此,可能结果也不一样。
事实表的种类
事实表有三种基本种类
- 事务事实表:也就是通常所说的事实表,每次事件发生时会记录一行(或多行)。比如订单表,每个订单会多一行
- 周期快照事实表:通常是将事务事实表以某一周期汇总后进行分析。比如工作中常见的以日为周期、周为周期或者月为周期。按周期汇总可以去除时间不同带来的影响,比方说周末的商品销售量和工作日的一般有很大不同;还可以减少数据量,提高分析的性能。
- 累积快照事实表:这一名称非常令人困惑,我觉得叫多步骤事实表可能更直观一些。它通常记录了一系列事件的状态流转过程,比方说一个商品采购事实表,可能需要记录某批次采购的状态,包括合同签订、供应商发货、分发到子仓库等步骤,每个步骤有一些关键维度(比如接收人是谁,接收商品数量、接收金额等)和关键日期。这个表就很适合存储这样的多步骤事实。
那么可能有人会问了,3#看起来就像是把几类不同的事实粘合在一起,能用多个1#类事实表替代吗?
作者提到,SQL的跨行分析能力很差。假如我们希望找出第一阶段和第二阶段之间的时间差大于5天的慢流程,那就需要做一次多事实表JOIN得到临时表,然后再在临时表上做进一步分析。如果某些事实本身就具有3#可应用的模式,那何必强行用1#呢?
事实表还有一些特殊形式,比如
- 无事实的事实表:我觉得称它为无度量的事实表更好。这样的事实表中可能没有数字,比如一个用户使用数据表,只记录用户某时某刻使用了App的某一功能。这样的表虽然没有数字,但还属于事实表——或者可以将其看成度量为“1”的事实表,即用户某时某刻使用了某一功能,一次。这个“1”也是可加的,比方说可以用于统计用户当天使用了各个功能次数的分布,找到用户最常使用的功能,因此不要觉得没有度量就不是事实表。
- 聚集事实表:我觉得叫聚合事实表更合适。聚集事实表通常在原始表上选择某些维度进行聚合,以达到提高性能的目的。比方说周期快照事实表相当于是在时间维度聚合的聚集事实表。
维度表建模技术
基本原则
避免用自然键作为维度表的主键,而要使用类似自增的整数键
自然键通常指的是维度信息中具有现实意义的某些列,它们能唯一指定维度表中的一行(或者某些行,在需要保留更改记录的情况下)。自然键非常直观,比方说用产品SKU作为商品表的主键,事实表都通过SKU与商品表关联。那么为什么作者建议不要使用自然键作为维度表主键呢?主要原因大概有:
- 自然键是从特定的业务背景产生的,使用自然键带来的假设很可能会在未来被违背。比如说商品的SKU可能会被重复利用,当某一商品下架后一段时间,这个SKU就可能代表另一个商品。这样的维度表会给使用者带来很大困惑。
- 方便从多个数据源中集成数据,不用考虑多个数据源中自然键的定义是否相同,是否会重复等
- 整数的JOIN操作性能很好
避免过度规范化
我们知道在SQL数据库建模领域有第一范式、第二范式、第三范式等。而在数仓建模领域,通常只遵循第一范式,只要每个列都存储基础类型就可以。换个术语来说,数仓领域通常是星型模型(事实表在中央,与一系列维度表关联,就像从一个点发出多条射线一样),而不是雪花模型(事实表在中央与多个维度表关联,维度表还和一系列维度表关联,就像雪花,每个子结构都相似)。
为什么需要避免过度规范化?主要从性能和简单性出发考虑:
- 一层JOIN的性能还不错,多层JOIN性能差,不方便进行实时分析
- 星型模型只有一层,方便使用者理解
如果有的使用者担心非规范化存储了很多重复值,浪费了很多空间,作者的意见是,维度表的容量相对于事实表少了几个数量级,因此无需在意空间的浪费。
作者在本书中还反复提到了支架表。支架表的想法和雪花模型比较类似,大概就是把维度表中一些重复的属性抽成单独的维度表,并与主维度表关联。作者强调支架表可以用,但是尽量不要用,否则可能是走在过度规范化的路上——作者既然叫它“支架”而不是雪花,说明一般来说主维度也就和另一个副维度相关联。如果关联了好几个副维度,那可就不是支架而是真雪花了。
避免变成形同事实表的维度表
什么时候维度表会形同事实表呢?通常是事实表和维度表都使用了同一主键的时候。比方说一个订单表存储了用户的一次购买行为(以订单为粒度),而设计者觉得应该把订单信息(比如订单号、订单日期、订单金额等我就瞎说一通了)单独放到一个订单维度表里,而事实表里存放订单维度键、用户维度键等。这时会发现,事实表和维度表的行数是相同的。
也就是说,当看到维度表和事实表一样大时,就要觉得有点不对劲了。这两个表实际上是同一个表,因此解决方法是把两个表的字段合一(因为它们粒度相同,主键相同,因此合一不会有任何问题),然后再考虑抽取维度。可以参考第11章-电信中的评审例子来理解这一原则
SCD(Slowly Changed Dimension)建模
第五章主要讲解了SCD建模问题。书中称之为缓慢变化维度,但实际上只关注维度变化的问题,而不只是“缓慢”变化维度。从这一章之后,书中会时不时提到“第二类变化维度”这种词,指的就是第五章介绍的这些SCD建模方法
事件表的修改通常是追加新的事件来增加行数,修改一般也是订正错误的事件,所以一般来说,修改历史事件不太需要很多讨论。但维度表通常会被多个事实表、多行关联,因此维度表的修改要考虑的问题更多。比如说
- 当维度表更新时,所有关联事件的维度都会被更新,这是预期的吗?比方说需要让历史数据的维度保持原样,新修改的维度只影响新数据吗?
- 维度表更新速度有多快?比方说用户维度表中,包含了一个更新频率很高的字段:“用户积分”,但是其它字段比如用户姓名、城市等几乎不变化。在这种情况下,如果需要记录历史变更记录,那整个维度会因为“用户积分”维度,导致变化很频繁,甚至变化频率都要和事实表不相上下了——这样导致的问题是维度表会记录太多历史数据,过分庞大,而且大部分变化都只针对一个维度,无效存储太多。这时候就要考虑将快速变化的部分从缓慢变化维度中抽离出来
上面大致介绍了变化维度建模需要考虑的事项,接下来介绍具体的建模方法。需要注意从这一章开始
- 不变化维度:这算是缓慢变化维度的一种特例:只追加新值,但从不变化。比方说日期表
- 直接修改原来的维度行:最暴力,但是会造成所有与之关联的事件,维度都更新,无法保留变更记录
- 为每一行加上有效的起止时间,若修改已有的维度行,则会插入新行,并将旧行的截止时间设为新行开始时间(end-time exclusive)。
- 添加新列,表示最近几次修改记录:比如一个员工信息表,可以用“上个部门”和“当前部门”两个列来记录变化,通常在只关心最近几次变动的场景下比较实用
- 将快速变化部分从缓慢变化维度中抽取出来,同时使用范围值替代确定值:比如刚刚说的用户积分,可以使用用户消费等级这样的维度替代,比如消费0-5000元的为初级用户,10w+的为忠实用户等,避免维度快速变化给维度表带来太多更新。如果需要计算用户的真实积分数值,可以使用周期快照表等方式实现。
- 混合以上维度的方法:通常一个维度表中的属性很多,每个属性的特点也不同,因此可以混合以上的处理方法来处理某个维度表。比方说,为了方便起见,有的属性虽然需要保留历史数据,但我们希望还可以快速获取它当前的值,比方说对于员工信息表,我们希望每一行都有当前员工所在部门,和历史时期员工所在部门,我们就可以混合类型1和2建立维度表。每次员工部门变化时,我们既要运用类型2方法插入一行表明当前员工部门变化的数据(并设置旧数据的结束时间),还要把“当前部门”这一列全都刷成最新值。除此之外还有一些混合建模技术,可以通过书来查看。
其它常见规则
这些可以直接看11.2节“设计评审的一般性考虑”和16.9节 “需要避免的常见维度建模错误”,作者做了比较好的总结。
- 避免使用原始操作代码或者缩写作为维度属性,要使用人类可读的文本。比方说要用Yes/No(或者True/False,是/否等)来代表是否,而不要使用0/1,或者T/F这样摸棱两可的符号来节约空间。这样是为了让维度值有更好的可读性,而且也方便用户在BI应用上自主地分析数据,而不用跑去问数据源团队这个符号到底是什么意思
- 不论事实表或维度表,它们粒度要一致,比方说不要在日精度的表里塞入周汇总或月汇总数据,这样很容易造成统计错误(比如求和、计数)并且让使用者迷惑
结语
工作之后也没有太多时间看书或者写总结,这篇笔记也写得比较粗糙。如果有什么说得不对的或者希望讨论的也可以直接提出来