深入解析数据仓库中的缓慢变化维
前言
最近公司在招聘数仓开发,笔者负责技术方面的一些问题,缓慢变化维 自然是是不可缺少的环节。
但出乎笔者预料的是,所有的面试者都没有完整了解 缓慢变化维 的前因后果及处理方式,大都是通过“野路子”碰运气实现几种简单通用的变化方式,甚至有人声称缓慢变化维就是拉链表。
因此,笔者将基于 kimball 的数仓理论和自身对其的理解,对缓慢变化维进行全面且深入的介绍。
什么是缓慢变化维?
要解释缓慢变化维,必须先解释什么是维度。
什么是维度?
在数据仓库的DW层中,表根据用途往往会分为2个类型:FACT(事实表)和 DIM(维度表)。
举个例子,如果我们要描述一个餐饮过程:
小明 2020年4月19日下午3点20分 在 海底捞(万达广场) 吃了5道菜,每道菜的单价是4元,总价是20元。
那么这个过程在数仓中,会如此划分:
- fact:餐饮过程,单价、数量、总价
- dim:小明,餐饮时间,餐饮门店,菜名。
也就是说:吃了多少东西,多少钱——这些属于fact;在哪里吃、什么时候吃?这些属于dim。
下面是简单的ER图,方便大家更好的理解。
image.png黄色为事实表,蓝色的就是维度表。
什么是缓慢变化维?
正如上述所言,我们会将分析的各种角度,存放在维度表中。但正如每个人所见,维度里的数据是可能发生变化的——尽管可能跨越极久。
举2个例子:
-
客户的性别变更
可能在第一次登陆中,我们得到的信息是 该客户性别为男。
但在几年的客户再一次使用中,我们又得到该客户的性别为女。
这就是维度值的一种变化可能
性别一般并不会改变,所以大概率是其中的一次数据有误。但也有可能是客户做了变性手术。
-
雇员的部门更替
假定有一个雇员叫小杨,他最早是负责运营的——此时他的title是"商品运营助理";但因为某些原因,他转组成为数据组的一员,这时title就变成了"数据分析专员"。
这是缓慢变化维的一种常见可能
上面提到的这些数据变化,业务系统(CRM、OA等)往往并不会保留历史数据。但在分析角度,我们是一定要保留这些改变的痕迹。这种随着时间可能会缓慢变化的维度,就是 缓慢变化维、也就是 SCD(Slowly Changing Dimensions)
常见的处理方法
kimball整理的处理方法一共有8种,但往往只有3种被详细使用。
类型1 重写
与业务数据保持一致,直接update为最新的数据。
这种方法主要应用于以下两种情况:
- 数据必须正确——例如用户的身份证号,如需要更新则说明之前录入错误。
- 无需考虑历史变化的维度——例如用户的头像url,这种数据往往并没有分析的价值。因此不做保留。
这种处理方式的优缺点:
- 优点:
- 简化ETL——直接update即可。
- 节省存储空间——其他存储方法都占用更多空间。
- 缺点:
- 无法保留历史痕迹——万一有天想分析呢?
类型2 增加新行
更新历史数据时间戳,新增新行记录新值。
这种方法主要用于 仅需要保存历史数据 的业务场景
具体的ETL则如下:
自然键即指有业务意义的唯一ID,例如用户ID、身份证号等。代理键则可以简单理解为该表的自增ID值
-
自然键第一次出现时。
新增一行数据,created为业务系统的创建时间,updated为9999-12-31
数仓的规范不允许数据存在NULL值的情况,因此用9999-12-31代替
-
类型2的维度发生变化时
-
将自然键当前记录的updated由9999-12-31刷为最新时间
-
新增一行记录,记录最新的数据,created为最新时间,updated为 9999-12-31
-
这样一来,因为事实表存储的是维度表的代理键而非自然键,因此在历史数据的查询中会以历史的维度值进行计算。同时在维度值更新后的相关数据自然使用的是新的代理键。完美的解决了大部分缓慢变化维情况。
类型3 增加当前值属性
在大部分的维度模式中,很多的源数据变化将产生类型1和类型2变化。有时两种技术都不能满足需求——当需要分析所有 伴随着新值或旧值的变化前后 记录的事实时,需要采用类型3变化。
很多人都难以理解类型3的重要性,因此笔者举一个例子——一个无法用类型1和类型2处理的例子:
假定一家公司的销售是按照销售区域进行分组:
image.png
突然有一天,领导灵机一动,决定 精细化销售,将东部、南部、北部重新划分为东南、东北部
image.png
但由于发送的过于仓促,因此销售人员是立刻使用了新的部门划分;但同样希望保留旧的名称——至少要暂时保留,用以比较今年和去年的业绩。即:
- 拥有使用 新区域 分析所有事实的能力,无论变化前还是变化后
- 拥有使用 旧区域 分析所有事实的能力,无论变化前还是变化后
第一个需求——新区域分析——允许立即采用新的分组,所有历史订单都能分为东南、东北等新类别;
第二个需求——旧区域分析——允许公司采用旧分组,所有的订单可以根据旧值分组——就好像一切都没发生过变化。
这时,就会发生一些问题:先前的技术不适合——无论是类型1还是类型2,都不能同时满足这两个需求;
- 类型1可以满足第一种需求,使用新值写旧值。但显然它无法实现第二个需求;
- 类型2则更糟,它不能满足任意一个需求——旧的事实和旧的维度相连;而新的维度值和以后的事实相连。毫无疑问,它既不能分析旧数据、也不能分析新数据。
此时引入 类型3 处理方法:新增字段同时储存新旧值。
image.png如果发生第二次变化,当前的current会被更新到previous中,新的变化值则会写入current。
类型3 不保存事实的历史内容
需要注意的是,类型3 的改变往往并不是一个仅此一次的过程——它能发生1次就有可能发生2次甚至更多次。类型3 变化只保护变化属性的一个旧版本,一旦发生第二次变化,第一次变化前的值就要被废弃了。如果想要用变化3 来实现更多的版本,那只能增加更多的列来实现(例如dpt_2018,dpt_2019)——这无疑是非常愚蠢的。因此,除非特定需要,应尽量避免使用类型3的变化。
其他类型
剩下的5种类型基本都不被采用,但值得一提。
类型0 不做调整
这里的数据定义与类型1类似,但不同点在于 类型0绝不允许ETL对该维度进行更新——你真要改的话就手动改表吧。
例如数仓中的代理键
类型4 微型维度
当变化频率加快时候,并且维度表包含几百万行的维度表。如果对变化的跟踪采用可靠的SCD2技术对浏览和查询性能具有负面影响——太多行且无必要。采用新的独立的维度表消除频繁分析或者频繁变化的属性,这一维度技术叫做微型维度。
例如employee的 年龄、薪资、税收金额
年龄每年一变,薪资、税收金额也经常改变
这里要注意:
-
这些“易变化”的值并非存储其准确的值,而是其范围值;
例如年龄,我们不会存“23",而是会存"20-30"。收入我们不会存”999“,而是”0-1000“。如果存准确值的话,数据量会过于高。从分析角度,我们往往也只需要一个模糊的范围即可。
如果需要记录准确值,可以考虑使用 无事实的事实表 单独记录。
-
微型维度没有自然键,只有值的笛卡尔积组合。
你不会在表中看到 user_id 这种自然键,因此类型4中微型维度只能在事实表中出现。
如果想把维度表和相关的微型维度连起来,那就是类型5了。
常见的微型维度表结构:
image.png可以看到,该微型维度是由 年龄和薪资的笛卡尔积组合构成。
你可以在建立时就将所有可能组合都预计算存入——缺点是表一开始就较大,但优点是省去了ETL的功夫
也可以出现一个存一个,维度表的稀疏性表明了实际数据量并不会那么多。
常见的维度表、微型维度、事实表组合:查看餐饮时雇员的职位和年龄。
image.png可以看到,微型维度表与维度表通过事实表相连,并不直接连接。
类型5 类型1+微型维度
类型5,即是将类型4与类型1组合起来的方法合并。
该技术的特点是增加当前微型维度主键作为主维度的一个属性。该属性在主维度中以类型1进行变化更新——从而避免主维度表行的爆炸增长
这样一来:
- 可以从主维度表获取到其对应的微型维度数据——虽然只有最新的。
- 可以从相关事实表中获取微型维度历史变更的信息。
类型6 类型1+类型2+类型3
类型6,即是将类型1、2、3的联合使用。
主要解决的业务场景是:
- 该维度列变化频次较高,但即使这样也希望历史业务能以最新的值来分析。
- 无法确定每次该维度的变更时间。
- 希望保留历史数据方便追溯
下图为例:
image.pngETL步骤:
- 记录需要如此处理的列,分别创建previous和current两列。
- 新数据插入时,current = previous.
- 该列有新值时,
- 历史previous使用类型2方法处理——旧数据只处理updated,新行存储新值。
- 当前current使用类型1方法处理——所有该自然键的值都刷成最新值。
类型7 双类型1+类型2
在上面的类型6有一个缺点——需要额外增加一个current列。
如果有大量的维度都需要如此处理的话就会有问题:假定该维度表有150列,如果我们都用类型6处理,则会变成300列——这无疑是令人无法接受的。
类型7就是 解决以上困难的。有很多种实现方法,下面会介绍2种比较常见的
-
双重外键——应用于类型1&类型2的维度表
事实表对于该维度表存储2个外键,如下图
image.png
这么一来,如果想要了解雇员在用餐发生瞬间历史的状态,关联至左边的维度表即可得知;如果想要以雇员最新的状态进行分析。则直接取右边的表即可。
最新视图获取:右边表可以通过视图展示(比如只取updated == '9999-12-31'的数据),也可以生成一个实际表来存储。
视图主键ID:对应的主键current_employee_id则有多种取法,笔者建议使用超自然键,如系统无超自然键的话也可以用普通的自然键来替代。
超自然键:更为持续的自然键;一般自然键是由OLTP系统生成,但他们有可能发生改变——例如员工离职又入职,他的userid自然会变化。因此,完整的OLAP系统会自己生成一个和实际事务对应的自然键——即超自然键。
-
单外键
也可以更节省一些——事实表连外键都不需要增加。
image.png
这种处理方法的主要难点在于右表——不再只需一个自然键。
下图是左表(实际维度表)的变化——标准的类型1&类型2
image.png下图是右表(最新视图)的变化
image.png可以看到,在右表的所有相关字段中,维度列无论是类型1、2都更新成了最新的值。
双重外键的处理方法需要事实表多一个值,单重外键则需要ETL制造出一个存储最新值的维度表(逻辑较复杂,且查询会较慢)。具体采用哪种方法需要视具体业务场景
总结
对以上类型做一个总结
SCD类型 | 维度表行动 | 对事实分析的影响 |
---|---|---|
类型0 | 属性值无变化 | 事实与原始值相关联 |
类型1 | 重写属性值 | 事实与当前值相关联 |
类型2 | 为新属性值增加新行 | 事实与发生时的有效值关联 |
类型3 | 增加新列来存储当前和原先值 | 事实与当前和先前值关联 |
类型4 | 增加对快速变化维的微型维度 | 事实与有效的变化范围关联 |
类型5 | 增加类型4和主维度表的类型1外键 | 事实与有效的变化范围关联 |
类型6 | 用类型3方法新增列,历史值用类型2处理,当前值用类型1处理 | 事实与有效的变化范围、当前值关联 |
类型7 | 增加储存最新值的维度表或视图 | 事实与有效的变化范围、当前值关联 |
其他杂谈
缓慢变化维的处理,可以说是数仓的最基本能力要求之一。类型1、2、3是基本;类型4、5、6、7则是拓展。