一、概述
从事数据仓库这个职业,每天就写写SQL搞搞数据处理。你要说这写SQL有什么知识点要掌握,能让人脱口而出,面试还会长被问起,大概就只有拉链表了。
之前在Maxcompute存储设计一文中提到过拉链表,相比于产品提供的存储压缩方式,切片表转为拉链表其实是存储“压缩”最好的一种方式。
但是拉链表是一种数据存储结构,如果之前未使用过这种结构,你会发现要实现这种存储结构,原来的表设计要做调整,并且ETL处理逻辑也要跟着调整。这就让本来相对简单的工作变得复杂起来了,所以,如果不是非常有必要这种存储结构很可能就会被弃用。
10年前,当我在数据仓库行业打工赚钱的时候,掌握这种结构是必备的技能。因为传统数据库和MPP数据库存储规模都有限,所以只有这种结构才能让数据仓库定义中的“保留历史变化的数据集合”这一特征。但是在大数据兴起之后,hadoop技术让底层技术平台存储规模从关系型数据库的单台和MPP数据库一般规模的几十台上升到了另外一个规模(成千上万台,比如我们的Maxcompute一个集群的规模就可以达到这个规模),所以,这个技术变得不是那么重要了。
可能读者心中会疑惑,难道服务器不是花钱买的么?凡事都讲成本产出比是工业思维,在互联网思维下不值得一提,快人一步就是生死差别,所以这些成本都被忽略了。但是,在眼下我们看到互联网快人一步决生死的场景逐渐淡去的时候,这些成本我们要不要找回来呢?在非互联网场景,在政府、传统企业中要不要使用这种结构呢?我认为是需要的,即便是在现在的互联网公司,我也觉得推广拉链表也是可以为公司降本增效出一份力。
二、拉链表简介
关系数据库的业务操作通常分为三类,一是记录一个对象,二是记录一个事件,三是记录一些代码参数。以时间维度去看这些表的记录数变化,记录对象的表的一般会有小幅增长,代码参数表则基本上不变,而事件表一般都是记录对象表中活跃对象的数倍,一年增加一亿行,五年就至少增加五亿行。理论上这些记录在关系型数据库都会被更新,但是变化频次是不同的。事件型表一般是在短周期内变化,例如一笔购物交易,交易结束后事件表就不会再被更新。记录对象的表则是长周期内会变化,只有这个对象消亡后才可能不再变化。代码参数表一般不会变化,只有系统版本更新变化,增加或者调整一些新的业务和功能时候才会变化。从保留历史的角度去看这三类表,如果空间不足,一般关系型数据库管理员会要求业务系统定期清理冷数据。而这里面清理的主要就是事件表中的历史,一般采用归档备份或者挪到其他归档库这种操作。
拉链表是数据仓库存储历史数据的一种结构。这种结构把表中的每行记录全部字段值作为一个ID,每一次更新都会产生一行新的记录,每一行记录值的生命周期被2个字段“开始日期”“结束日期”记录下来。使用的时候,只需要卡住所需数据在“开始日期”“结束日期”区间内,这行记录就是有效的。这种结构,相对另外一种结构,就是切片表结构。切片表结构就是以分区表为基础的结构,每个最新的日期都会存储一份最新的全量数据,使用的时候只需要访问对应分区就可以获取所需日期分区的数据。相比拉链表,切片表结构随着存储日期数据的变多,其中那些记录值不会变化的数据会被存储多份。如果每天存储一个分区,一年就会被存储365行。这种方式对于那些已经不再更新的记录来说,就是巨大的存储浪费。并且随着存储周期的拉长,这种重复存储会不断增长。
我们可以看到,相比拉链表方案切片表方案是一种十分浪费存储的方案。但是我们日常做数据开发,其实很少遇到拉链表。这是为什么呢?
在数据仓库中,这三类表的数据加工ETL过程会被分层架构分解为多层架构来实现。包括:贴源层(临时层+镜像层)、中间层(数据明细层+数据汇总层)、应用层。这种分层架构会让数据加工的过程分解为一些独立的步骤,每一层只需要解决这个步骤需要解决的问题即可,让复杂的处理变得简单,也让数仓的数据和能力复用。
贴源层细分为临时层、镜像层,临时层就是数据第一次在数仓平台落地的存储。这部分数据一般就是以源系统数据发生变化产生的增量数据为主,本身不存在什么冗余。当然也有直接抽取全量业务系统数据库的情况,这种情况临时层和镜像层就是同一层了。镜像层简明扼要就是业务系统表的镜像。如果是数据集成任务全量抽取业务系统的表,那么抽取过来的数据就是业务系统表在抽取时点的一份镜像。如果是抽取增量,那么当日增量合并上日全量就是今日全量。镜像层一般在数据仓库都是切片存储的,根据实际业务需求存储时间是不定的。在大数据兴起之前,我所在参数的银行数仓项目,一般不会超过一个月(少量表,支撑一些纯需要镜像层数据的引用)。
中间层细分为明细层(也有叫基础层)、汇总层,这两层使用贴源层数据加工而来。根据架构细节不同明细层使用不同的贴源层数据加工,使用拉链表结构会使用临时层增量数据,使用切片表结构的则会使用镜像层数据加工。汇总层不会使用拉链表结构,因为汇总层的加工一般较为复杂,涉及聚合、多表关联。如果使用这种结构存储,会让业务开发人员要花费过多精力去构造拉链结构而不是关注业务本身。总的一句话就是:复杂易出错,利益和成本相比是负的。
模型层与汇总层是一个逻辑,仍然不适合使用拉链表(不是绝对,但是代表了绝大部分场景)。
通过上面数仓分层架构中使用拉链表场景的介绍,我们可以看到,真正能适用拉链表的场景就是分层架构中的明细层(基础层)。确实之前我在做银行数仓开发的时候,也只是在这一层使用了拉链表。当时汇总层之前数据在集群的存储占比只有整个存储的20%-30%,大部分存储都是被后面的应用层使用了。
三、拉链表在数仓开发中的实现
拉链表在网上可以搜到不少文章,只是没想到都是基于Hive的。其实我倒是想搜一下之前我做数仓开发时候在关系型数据库的例子,做个对比。奈何现在搞数仓应该最流行Hive了,所以,只查到了这一种。好在Maxcompute也其实就是Hive的阿里版本,所以,这种例子是可以直接拿来用得。
网上文章搜到的场景其实都是一种,就是之前在做数仓基础层的时候使用的,有简单的也有复杂的,但是其实都是一种算法。我记得之前我们的算法(不同场景实现方法)能有五六种,还得好好学习一下。
我找了一个最简洁的SQL实现方法如下:
INSERT OVERWRITE TABLE table_hisroty PARTITION(ds='${bizdate}')
SELECT * FROM
(
SELECT a.columns --【除了start_date,end_date之外的字段】
,a.start_date
--【关链】
--今天有更新记录的主键,该主键上日还有效(end_date是未来时点'3000-12-31'),这条记录的结束日期被更新为今天,否则还是之前的end_date
,CASE WHEN a.end_date = '3000-12-31' AND B.pk IS NOT NULL THEN '${bizdate}'
ELSE a.end_date
END AS end_date
FROM table_hisroty a
LEFT JOIN table_update b
ON a.pk = b.pk
UNION
SELECT b.columns --【除了start_date,end_date之外的字段】
--【新开链】
--今天有更新的记录,生成一条心的拉链数据,开始日期是今天,结束日期是未来时点'3000-12-31'
,'${bizdate}' AS start_date
,'3000-12-31' AS end_date
FROM ods.user_update b
)t;
参考:https://blog.csdn.net/z69183787/article/details/124480770
其实这个逻辑与我之前在在《如何正确做增量加工》一文中的MERGE逻辑非常类似,新增数据插入,merge旧的与新增数据同PK的数据直接就排除了,而拉链逻辑是关链继续保留。
四、拉链表在应用中的简单性与复杂性
参考上面这段实现SQL,你就可以快速的让你现在切片存储的基础层改造为拉链表了。并且这段SQL其实很简单,按照我的经验这些代码其实都是可以动态生成的,不同的表只需配置一下就可以像实现merge逻辑一样动态生成SQL。好像也不需要什么开发工作量,真的很简单。
但是这种结构一旦中途发现历史某个时点数据有问题,怎么办?这种情况,需要一天一天的把拉链表再重新跑出来。因为这个结构的复杂,你很难像切片表一样批量处理,只能顺序修复一下。
还有在使用中,如果你发现数据有异常,要排查问题的时候,这种结构初用其实也有些头疼。开始时间、结束时间大于号小于号写错,都会让数据搞错,所以,还得熟悉一下才能正常使用。虽然仔细看,其使用起来与切片表差异并不大。
SQL如下:
--切片表
select * from table_hisroty where ds ='${bizdate}';
--拉链表
select * from table_hisroty where '${bizdate}'>=start_date and '${bizdate}'<end_date;
五、结尾
关于拉链表使用的这内容就到这里了,个人还是建议明细层(基础层)存储较大的用户考虑一下这种结构。毕竟使用切片表存储的数据,冗余的还是挺多的。
以常见的T+1批量为例。切片表如果按日存储切片历史数据,一年365份的重复数据存储会让存储不堪重负。而如果采用常用的保留月末,其余时点删除的策略,其实一年只保留了12天的时点数据。如果使用拉链表,则可以保留一年365天的所有日切片时点数据,又只是占用了应该占用的存储,一点都没有浪费。
稍微增加一点点复杂,能节约这么多的空间,何乐而不为呢?为老板和客户省点钱,也是体现一下自己价值的方法。