深入解析数据仓库中的缓慢变化维

前言

最近公司在招聘数仓开发,笔者负责技术方面的一些问题,缓慢变化维 自然是是不可缺少的环节。

但出乎笔者预料的是,所有的面试者都没有完整了解 缓慢变化维 的前因后果及处理方式,大都是通过“野路子”碰运气实现几种简单通用的变化方式,甚至有人声称缓慢变化维就是拉链表

因此,笔者将基于 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为最新的数据。

这种方法主要应用于以下两种情况:

  1. 数据必须正确——例如用户的身份证号,如需要更新则说明之前录入错误。
  2. 无需考虑历史变化的维度——例如用户的头像url,这种数据往往并没有分析的价值。因此不做保留。
image.png

这种处理方式的优缺点:

  • 优点:
    • 简化ETL——直接update即可。
    • 节省存储空间——其他存储方法都占用更多空间。
  • 缺点:
    • 无法保留历史痕迹——万一有天想分析呢?

类型2 增加新行

更新历史数据时间戳,新增新行记录新值。

这种方法主要用于 仅需要保存历史数据 的业务场景

具体的ETL则如下:

自然键即指有业务意义的唯一ID,例如用户ID、身份证号等。代理键则可以简单理解为该表的自增ID值

  • 自然键第一次出现时。

    新增一行数据,created为业务系统的创建时间,updated为9999-12-31

    数仓的规范不允许数据存在NULL值的情况,因此用9999-12-31代替

image.png
  • 类型2的维度发生变化时

    1. 将自然键当前记录的updated由9999-12-31刷为最新时间

    2. 新增一行记录,记录最新的数据,created为最新时间,updated为 9999-12-31

image.png

这样一来,因为事实表存储的是维度表的代理键而非自然键,因此在历史数据的查询中会以历史的维度值进行计算。同时在维度值更新后的相关数据自然使用的是新的代理键。完美的解决了大部分缓慢变化维情况。

类型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的 年龄、薪资、税收金额

年龄每年一变,薪资、税收金额也经常改变

这里要注意:

  1. 这些“易变化”的值并非存储其准确的值,而是其范围值;

    例如年龄,我们不会存“23",而是会存"20-30"。收入我们不会存”999“,而是”0-1000“。如果存准确值的话,数据量会过于高。从分析角度,我们往往也只需要一个模糊的范围即可。

    如果需要记录准确值,可以考虑使用 无事实的事实表 单独记录。

  2. 微型维度没有自然键,只有值的笛卡尔积组合。

    你不会在表中看到 user_id 这种自然键,因此类型4中微型维度只能在事实表中出现。

    如果想把维度表和相关的微型维度连起来,那就是类型5了。

常见的微型维度表结构:

image.png

可以看到,该微型维度是由 年龄和薪资的笛卡尔积组合构成。

你可以在建立时就将所有可能组合都预计算存入——缺点是表一开始就较大,但优点是省去了ETL的功夫

也可以出现一个存一个,维度表的稀疏性表明了实际数据量并不会那么多。

常见的维度表、微型维度、事实表组合:查看餐饮时雇员的职位和年龄。

image.png

可以看到,微型维度表与维度表通过事实表相连,并不直接连接

类型5 类型1+微型维度

类型5,即是将类型4与类型1组合起来的方法合并。
该技术的特点是增加当前微型维度主键作为主维度的一个属性。该属性在主维度中以类型1进行变化更新——从而避免主维度表行的爆炸增长

image.png

这样一来:

  1. 可以从主维度表获取到其对应的微型维度数据——虽然只有最新的。
  2. 可以从相关事实表中获取微型维度历史变更的信息。

类型6 类型1+类型2+类型3

类型6,即是将类型1、2、3的联合使用。

主要解决的业务场景是:

  1. 该维度列变化频次较高,但即使这样也希望历史业务能以最新的值来分析
  2. 无法确定每次该维度的变更时间。
  3. 希望保留历史数据方便追溯

下图为例:

image.png

ETL步骤:

  1. 记录需要如此处理的列,分别创建previous和current两列。
  2. 新数据插入时,current = previous.
  3. 该列有新值时,
    • 历史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则是拓展。

你可能感兴趣的:(深入解析数据仓库中的缓慢变化维)