vlambda博客
学习文章列表

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



作者 | 李谦恒


数据工程师。逻辑重于代码,高效胜过勤奋。崇尚 life work balance。



前言

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

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

因此,笔者将基于 kimball 的数仓理论和自身对其的理解,对缓慢变化维进行全面且深入的介绍。

什么是缓慢变化维?

要解释缓慢变化维,必须先解释什么是维度。

什么是维度?

在数据仓库的 DW 层中,表根据用途往往会分为 2 个类型:FACT(事实表)和 DIM(维度表)。

举个例子,如果我们要描述一个餐饮过程:

 小明 2020年4月19日下午3点20分 在 海底捞(万达广场) 吃了5道菜,每道菜的单价是4元,总价是20元。

那么这个过程在数仓中,会如此划分:

  • fact:餐饮过程,单价、数量、总价

  • dim:小明,餐饮时间,餐饮门店,菜名。

也就是说:吃了多少东西,多少钱——这些属于 fact;在哪里吃、什么时候吃?这些属于 dim。

下面是简单的 ER 图,方便大家更好的理解。

黄色为事实表,蓝色的就是维度表。

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

什么是缓慢变化维?

正如上述所言,我们会将分析的各种角度,存放在维度表中。但正如每个人所见,维度里的数据是可能发生变化的——尽管可能跨越极久。

举2个例子:

  • 客户的性别变更

    可能在第一次登陆中,我们得到的信息是 该客户性别为男。

    但在几年的客户再一次使用中,我们又得到该客户的性别为女。

    这就是维度值的一种变化可能

    性别一般并不会改变,所以大概率是其中的一次数据有误。但也有可能是客户做了变性手术。

  • 雇员的部门更替

    假定有一个雇员叫小杨,他最早是负责运营的——此时他的 title 是"商品运营助理";但因为某些原因,他转组成为数据组的一员,这时 title 就变成了"数据分析专员"。

    这是缓慢变化维的一种常见可能

上面提到的这些数据变化,业务系统(CRM、OA等)往往并不会保留历史数据。但在分析角度,我们是一定要保留这些改变的痕迹。这种随着时间可能会缓慢变化的维度,就是 缓慢变化维、也就是 SCD(Slowly Changing Dimensions)

常见的处理方法

kimball 整理的处理方法一共有 8 种,但往往只有 3 种被详细使用。

类型1 重写

与业务数据保持一致,直接 update 为最新的数据。

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

  1. 数据必须正确——例如用户的身份证号,如需要更新则说明之前录入错误。

  2. 无需考虑历史变化的维度——例如用户的头像 url,这种数据往往并没有分析的价值。因此不做保留。

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

这种处理方式的优缺点:

  • 优点:

    • 简化 ETL ——直接 update 即可。

    • 节省存储空间——其他存储方法都占用更多空间。

  • 缺点:

    • 无法保留历史痕迹——万一有天想分析呢?

类型2 增加新行

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

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

具体的 ETL 则如下:

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

  • 自然键第一次出现时。

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

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

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

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

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

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

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

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

类型3 增加当前值属性

在大部分的维度模式中,很多的源数据变化将产生类型 1 和类型 2 变化。有时两种技术都不能满足需求——当需要分析所有 伴随着新值或旧值的变化前后 记录的事实时,需要采用类型 3 变化。

很多人都难以理解类型 3 的重要性,因此笔者举一个例子——一个无法用类型 1 和类型 2 处理的例子:

假定一家公司的销售是按照销售区域进行分组:

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

突然有一天,领导灵机一动,决定 精细化销售,将东部、南部、北部重新划分为东南、东北部

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

但由于发送的过于仓促,因此销售人员是立刻使用了新的部门划分;但同样希望保留旧的名称——至少要暂时保留,用以比较今年和去年的业绩。即:

  • 拥有使用 新区域 分析所有事实的能力,无论变化前还是变化后

  • 拥有使用 旧区域 分析所有事实的能力,无论变化前还是变化后

第一个需求——新区域分析——允许立即采用新的分组,所有历史订单都能分为东南、东北等新类别;

第二个需求——旧区域分析——允许公司采用旧分组,所有的订单可以根据旧值分组——就好像一切都没发生过变化。

这时,就会发生一些问题:先前的技术不适合——无论是类型1还是类型2,都不能同时满足这两个需求;

  • 类型 1 可以满足第一种需求,使用新值写旧值。但显然它无法实现第二个需求;

  • 类型 2 则更糟,它不能满足任意一个需求——旧的事实和旧的维度相连;而新的维度值和以后的事实相连。毫无疑问,它既不能分析旧数据、也不能分析新数据。

此时引入类型 3 处理方法:新增字段同时储存新旧值。

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

如果发生第二次变化,当前的 current 会被更新到 previous 中,新的变化值则会写入 current。

需要注意的是:类型 3 不保存事实的历史内容

需要注意的是,类型 3 的改变往往并不是一个仅此一次的过程——它能发生 1 次就有可能发生 2 次甚至更多次。类型 3 变化只保护变化属性的一个旧版本,一旦发生第二次变化,第一次变化前的值就要被废弃了。如果想要用变化 3 来实现更多的版本,那只能增加更多的列来实现(例如 dpt2018,dpt2019)——这无疑是非常愚蠢的。因此,除非特定需要,应尽量避免使用类型 3 的变化。

其他类型

剩下的 5 种类型基本都不被采用,但值得一提。

类型 0 不做调整

这里的数据定义与类型 1 类似,但不同点在于 类型 0 绝不允许 ETL 对该维度进行更新——你真要改的话就手动改表吧。

例如数仓中的代理键

类型4 微型维度

当变化频率加快时候,并且维度表包含几百万行的维度表。如果对变化的跟踪采用可靠的 SCD2 技术对浏览和查询性能具有负面影响——太多行且无必要。采用新的独立的维度表消除频繁分析或者频繁变化的属性,这一维度技术叫做微型维度。

例如 employee 的 年龄、薪资、税收金额

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

这里要注意:

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

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

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

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

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

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

常见的微型维度表结构:

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

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

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

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

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

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

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

类型5 类型1+微型维度

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

这样一来:

  1. 可以从主维度表获取到其对应的微型维度数据——虽然只有最新的。

  2. 可以从相关事实表中获取微型维度历史变更的信息。

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

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

主要解决的业务场景是:

  1. 该维度列变化频次较高,但即使这样也希望历史业务能以最新的值来分析。

  2. 无法确定每次该维度的变更时间。

  3. 希望保留历史数据方便追溯

下图为例:

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

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 个外键,如下图

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

    这么一来,如果想要了解雇员在用餐发生瞬间历史的状态,关联至左边的维度表即可得知;如果想要以雇员最新的状态进行分析。则直接取右边的表即可。

    最新视图获取:右边表可以通过视图展示(比如只取 updated == '9999-12-31'的数据),也可以生成一个实际表来存储。

    视图主键ID:对应的主键 currentemployeeid 则有多种取法,笔者建议使用超自然键,如系统无超自然键的话也可以用普通的自然键来替代。

    超自然键:更为持续的自然键;一般自然键是由 OLTP 系统生成,但他们有可能发生改变——例如员工离职又入职,他的 userid 自然会变化。因此,完整的 OLAP 系统会自己生成一个和实际事务对应的自然键——即超自然键。

  • 单外键

    也可以更节省一些——事实表连外键都不需要增加。

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

    这种处理方法的主要难点在于右表——不再只需一个自然键。

    下图是左表(实际维度表)的变化——标准的类型 1&类型 2

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

    下图是右表(最新视图)的变化

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

    可以看到,在右表的所有相关字段中,维度列无论是类型1、2 都更新成了最新的值。

    双重外键的处理方法需要事实表多一个值,单重外键则需要ETL制造出一个存储最新值的维度表(逻辑较复杂,且查询会较慢)。具体采用哪种方法需要视具体业务场景

总结


全文完



以下文章您可能也会感兴趣:




我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 [email protected]