漫谈纬度表如何设计(三)
一:缓慢变化维如何处理?
有了之前两篇纬度设计的知识铺垫,这篇就会相对于好理解一些。实际的业务是很复杂的,如果按照之前的建模理论来设计纬度表,设计好了,一段时间看似没有问题,但是随着业务的发展,纬度表同样会面临着海量数据的存储,比如说公司已经有几亿的用户,那么这时公司的纬度表至少也有几亿条记录,更为糟糕的是,纬度表的纬度属性并不是一层不变的,像商品主纬度表这样的,商品的所属类目,售价,销售状态(上架还是下架),这些都是会变化的,针对这种变化纬度该如何处理?保存每天最新快照状态,意味着失去历史数据,想要历史数据可追溯,每天保存一份最新的快照数据?这对存储开销简直是灾难。根据之前文章的介绍,觉得可以用主从纬度表解决这个问题,把经常变化的纬度属性放于从纬度表,这样其实并解决不了问题,仔细想一想,举个例子,假设现在有一张商品主纬度表,还有一张类目从纬度表,商品主纬度表里面有个类目id可以关联到类目从纬度表,如何商品纬度表的类目发生变化,类目id还是需要修改的,也就是说商品主纬度表的记录还是需要更新。那么主从纬度表主要是解决什么问题,比如说我现在要新增商品类目,那么只需要在类目表新增就可以了,商品主纬度表不需要做更改。既然问题已经抛出,接下来看下传统(即不是最优)解决这个问题的方式:
- 重写纬度值
这种方式会覆盖掉历史的记录,导致历史不可追溯,比如说商品1-》类目1,后来更新为商品1-》类目2,如果我现在有个需求,统计类目1下的销售额,那是不是商品1销售的部分就没有统计到。但是如果不考虑 - 插入新的记录行
为了保存历史记录的话,可以选择插入一行新的记录,这样虽然可以保存历史,但是也会存在问题,第一:还是上面的例子,商品1-》类目1,后来更新为商品1-》类目2,如果需要你统计类目2下的销售额,并且说明包含商品历史类目的销售额,那么我就需要将之前类目1的销售额,这样就不太好操作了,因为你并不知道这个商品之前所在类目。 - 增加新的纬度属性列
增加一个纬度属性,比如说还是之前的例子,把类目一放在旧类目,类目2放在新类目,这样是可以很方便统计到之前的那个需求,但是也有限制,万一又发生了变化呢,增加一个类目3,那么的列数就不固定了,不过加工的时候,可以这样处理,分为旧类目,新类目,就类目里面放一个list,这样就可以满足不确定列数的问题,但是同时增加了使用数据的难度。
二:快照纬度的优弊
缓慢变化维的问题已经解释清楚了,传统的解决办法也各自说明了利弊。那么继续探究缓慢变化纬的最优解决,可能有人能想到,那就直接保存每天最新的数据,以天做为分区,这样确实可以解决很多问题,比如我们在计算的商品销售额的时候,可以取到任何一天商品的所属类目,那么想要计算这个商品所有历史类目的销售额也很简单,拿销售事实表的商品id和时间和商品表关联,能关联上的都是这个商品下的销售额,在用聚合函数解决就是了。但是快照纬度也有致命的问题,就是太耗费存储空间了,考虑极端的情况,如果纬度表每天一条数据都不变化,每天都会存储一份全量的数据,这是对存储空间的浪费。
三:拉链表的优弊
由快照纬度的缺点,引出新的解决方案,那就是使用拉链表,关于拉链表的介绍本文不做详述了。下面说说拉链表的优弊,优点很明显,拉链表的出现就是为了解决缓慢变化维出现的,只保存变化的记录,极大的节省了存储的空间,并且还可以保存历史,但是前提是变化的记录并不多,如果变化的记录特别多,其实拉链表的作用也不是特别的大,所以我们一般在加工拉链表的时候,会移除经常发生变化的纬度属性。拉链表其实还有一个缺点,就是对下游的使用很不方便,因为熟悉拉链表的人都知道,为了保存这条记录的有效期,一般都会带上start_day和end_day,end_day如果是9999-12-31(或者是其它一个极大的值)表示该条记录是最新的。那么如果下游的用户要取最新记录,还要筛选start_day和end_day,这其实对于不懂数据建模的人来说,是很难的理解的。为了加快拉链表的索引速度,如果根据start_day和end_day作为分区键,那么一年下来分区数就有365*(364/2)=66430,也是非常恐怖的。
四:极限存储如何实现?
拉链表其实已经基本满足我们对缓慢变化维的解决方案了,但是还不够完美,就像上面说到拉链表到弊端一样。我们一一来整理一下:
- 节省存储空间不明显
将经常发生变化的纬度属性移除,拉链表里面不应该存在经常发生变化的纬度属性。 - 下游用户不方便使用
我们基于拉链表做一个视图,简化用户的操作,屏蔽对拉链表底层的逻辑。还有一个就是基于hive hook,hook类似于hive里面的扩展接口,也可以叫做钩子函数,比如说前置钩,在hive执行sql之前,截取sql,进行自己sql-》拉链表逻辑sql的一个映射,在用hive执行,这样也是可以的。 - 分区数过大
分月做拉链表,以每个月为一个周期,这样的分区就为12*(1+(30+29)/2)=5232个