数仓拉链表超详细讲解(通俗易懂)

拉链表

一丶什么是拉链表

拉链表是一种数据模型,主要是针对数据仓库设计中表存储数据的方式而定义的,顾名思义,所谓拉链,就是记录历史。记录一个事物从开始,一直到当前状态的所有变化的信息。拉链表可以避免按每一天存储所有记录造成的海量存储问题,同时也是处理缓慢变化数据的一种常见方式。

百度百科的解释:拉链表是维护历史状态,以及最新状态数据的一种表,拉链表根据拉链粒度的不同,实际上相当于快照,只不过做了优化,去除了一部分不变的记录,通过拉链表可以很方便的还原出拉链时点的客户记录。

二丶拉链表的产生背景

在数据仓库的数据模型设计过程中,经常会遇到这样的需求:

  1. 数据量比较大
  2. 表中的部分字段会被update,如用户的地址,产品的描述信息,订单的状态等等;
  3. 需要查看某一个时间点或者时间段的历史快照信息,比如,查看某一个订单在历史某一个时间点的状态,比如,查看某一个用户在过去某一段时间内,更新过几次等等;
  4. 变化的比例和频率不是很大,比如,总共有1000万的会员,每天新增和发生变化的有10万左右;
  5. 如果对这边表每天都保留一份全量,那么每次全量中会保存很多不变的信息,对存储是极大的浪费;

对于这种表有几种方案可选: 方案一:每天只留最新的一份,比如我们每天用Sqoop抽取最新的一份全量数据到Hive中。 方案二:每天保留一份全量的切片数据。 方案三: 每天保存一份增量数据 方案四:使用拉链表。

以上方案对比

方案一

这种方案就不用多说了,实现起来很简单,每天drop掉前一天的数据,重新抽一份最新的。

优点很明显,节省空间,一些普通的使用也很方便,不用在选择表的时候加一个时间分区什么的。

缺点同样明显,没有历史数据,先翻翻旧账只能通过其它方式,比如从流水表里面抽。

方案二

每天一份全量的切片是一种比较稳妥的方案,而且历史数据也在。

缺点就是存储空间占用量太大太大了,如果对这边表每天都保留一份全量,那么每次全量中会保存很多不变的信息,对存储是极大的浪费,这点我感触还是很深的…

当然我们也可以做一些取舍,比如只保留近一个月的数据?但是,需求是无耻的,数据的生命周期不是我们能完全左右的。

方案三

每天都保存增量数据,这种方案相比较方案一二的话,数据量变少了,也记录了每条数据的变化.但是数据量还是比拉链表多,同时它要求某天的历史数据查询效率比较低,比较繁琐.比如你要求2021年10月01号的在职人数,你就需要判断入职日期小于等于10月01号的,用lead函数获取下条数据,判断下条数据的离职日期是否大于2021年10月01号.

拉链表

拉链表在使用上基本兼顾了我们的需求。

首先它在空间上做了一个取舍,虽说不像方案一那样占用量那么小,但是它每日的增量可能只有方案二的千分之一甚至是万分之一。

其实它能满足方案二所能满足的需求,既能获取最新的数据,也能添加筛选条件也获取历史的数据。

所以我们还是很有必要来使用拉链表的。

三丶在Hive中实现拉链表 在现在的大数据场景下,大部分的公司都会选择以Hdfs和Hive为主的数据仓库架构。目前的Hdfs版本来讲,其文件系统中的文件是不能做改变的,也就是说Hive的表智能进行删除和添加操作,而不能进行update。基于这个前提,我们来实现拉链表。

还是以上面的用户表为例,我们要实现用户的拉链表。在实现它之前,我们需要先确定一下我们有哪些数据源可以用。

  1. 我们需要一张ODS层的用户全量表。至少需要用它来初始化。
  2. 每日的用户更新表。 而且我们要确定拉链表的时间粒度,比如说拉链表每天只取一个状态,也就是说如果一天有3个状态变更,我们只取最后一个状态,这种天粒度的表其实已经能解决大部分的问题了。

另外,补充一下每日的用户更新表该怎么获取,据笔者的经验,有3种方式拿到或者间接拿到每日的用户增量,因为它比较重要,所以详细说明:

  1. 我们可以监听Mysql数据的变化,比如说用Canal,最后合并每日的变化,获取到最后的一个状态。
  2. 假设我们每天都会获得一份切片数据,我们可以通过取两天切片数据的不同来作为每日更新表,这种情况下我们可以对所有的字段先进行concat,再取md5,这样就ok了。
  3. 流水表!有每日的变更流水表。

拉链表制作过程图解

四丶实例讲解 需求:现在需要把一张每天存有全量数据的表制作成拉链表 步骤: 1.先把已有的全量分区表改造为拉链表①

--创建岗位状态表,在第一次执行脚本执行,这段sql,先在已有的数据上做拉链表,之后用拉链表去和每日新增及变化的数据进行合并
--注意:如果任务执行失败,那么可以重新执行这段sql制作拉链表,结束时间改为最新时间就好了
drop table if exists tmp.ems_base;
create table  tmp.ems_base as
select
t3.*
,case when t3.hire_date >= t3.date_from and  t3.cancel_flag <> 'Y' then '入职'   
when t3.hire_date < t3.date_from and t3.cancel_flag <> 'Y' then '调岗'
when t3.cancel_flag = 'Y' then '离职'
else '其它'   end  as  type        --给每条数据标上一个状态类型,这里面的判断你们不用理解,只需要知道缓慢变化的维度是这个字段(入职,调岗,离职)三种类型
from
(select
t2.*
,row_number() over(partition by t2.emp_num,t2.date_from,t2.cancel_flag,t2.hire_date order by t2.inc_day) as rn
from
(select
lpad(t1.emp_num ,8,'0') as emp_num    --工号(没有8位的用0补齐)
,t1.curr_org_id       --当前部门组织id
,t1.org_code     --组织编码
,t1.curr_org_name     --组织名称
,t1.hire_date       --入职日期
,if(t1.date_from < t1.hire_date,t1.hire_date,t1.date_from)  as  date_from    --调入当前网络时间
,if(t1.cancel_flag = 'Y',t1.cancel_date,'00000000') as cancel_date   --离职日期
,t1.cancel_flag     --离职标识
,t1.zhrlzlx         --离职类型
,t1.inc_day
from 
(select
*
from  ods.etl_ems    --人员全量分区表
where inc_day between 开始时间 and 结束时间
) t1
) t2 
)t3
where rn = 1    
;  
--创建拉链表
drop table if exists ods.ems_zipper;
create table ods.ems_zipper as 
select 
t1.emp_num             --工号
,t1.curr_org_id       --当前部门组织id
,t1.org_code          --组织编码
,t1.curr_org_name     --组织名称
,t1.hire_date         --入职日期
,t1.date_from         --入网时间
,t1.cancel_date       --离职时间
,t1.cancel_flag       --离职标识
,t1.type              --(入职,调岗,离职)
,t1.zhrlzlx           --离职类型
,from_unixtime(unix_timestamp(case when t1.type = '调岗' then t1.date_from 
when t1.type = '入职' then t1.hire_date
when t1.type = '离职' then t1.cancel_date                --因为这三种状态的时间存储在不同字段当中,所以需要去判断是哪种状态,然后获取哪个时间字段作为开始时间
end  
,'yyyyMMdd'),'yyyy-MM-dd')                   as begin_date  --开始日期 
,case when lead(t1.type,1,'无') over(partition by t1.emp_num order by t1.inc_day) = '调岗' then date_sub(from_unixtime(unix_timestamp(lead(t1.date_from,1,'99991231') over(partition by t1.emp_num order by t1.inc_day),'yyyyMMdd'),'yyyy-MM-dd'),1) 
when lead(t1.type,1,'无') over(partition by t1.emp_num order by t1.inc_day) = '离职' then date_sub(from_unixtime(unix_timestamp(lead(t1.cancel_date,1,'99991231') over(partition by t1.emp_num order by t1.inc_day),'yyyyMMdd'),'yyyy-MM-dd'),1) 
when lead(t1.type,1,'无') over(partition by t1.emp_num order by t1.inc_day) = '入职' then date_sub(from_unixtime(unix_timestamp(lead(t1.hire_date,1,'99991231') over(partition by t1.emp_num order by t1.inc_day),'yyyyMMdd'),'yyyy-MM-dd'),1) 
else '9999-12-30'     end  as end_date --结束日期
from  tmp.ems_base t1
;

2.通过昨天的全量数据和前天的全量数据比较,获取新增及变化的临时表②

-----创建每日新增及变化的表
drop table if exists tmp.ems_change;
create table tmp.ems_change as 
select
lpad(t2.emp_num,8,'0') as emp_num     --工号
,t2.curr_org_id       --当前部门组织id
,t2.org_code     --组织编码
,t2.curr_org_name     --组织名称
,t2.hire_date       --入职日期
,if(t2.date_from < t2.hire_date,t2.hire_date,t2.date_from)  as  date_from       --调入当前网络时间
,if(t2.cancel_flag = 'Y',t2.cancel_date,'00000000')  as cancel_date  --离职日期 
,t2.cancel_flag     --离职标识
,t2.zhrlzlx         --离职类型 
,case   when t2.hire_date >= t2.date_from and  t2.cancel_flag <> 'Y' then '入职'
when t2.hire_date < t2.date_from and t2.cancel_flag <> 'Y' then '调岗'
when t2.cancel_flag = 'Y' then '离职'
else '其它'   end  as  type
from
(select
t1.emp_num     --工号
,t1.curr_org_id       --当前部门组织id
,t1.org_code     --组织编码
,t1.curr_org_name     --组织名称
,t1.hire_date       --入职日期
,t1.date_from       --调入当前网络时间
,t1.cancel_date     --离职日期
,t1.cancel_flag     --离职标识
,t1.zhrlzlx         --离职类型
from ods.etl_ems  t1
where t1.inc_day = '${v_day_1ago}'
) t2
left join
(select
t4.emp_num     --工号
,t4.curr_org_id       --当前部门组织id
,t4.org_code     --组织名称
,t4.curr_org_name     --组织代码
,t4.hire_date       --入职日期
,t4.date_from       --调入当前网络时间
,t4.cancel_date     --离职日期
,t4.cancel_flag     --离职标识
,t4.zhrlzlx         --离职类型
from ods.etl_ems  t4
where t4.inc_day = '${v_day_2ago}'
) t3
on
t2.emp_num = t3.emp_num
--注意用concat时,只要有一个字段为null那么,拼接的结果就是null,所以需要nvl把空值先转换为字符串
where concat(nvl(t2.date_from,''),nvl(t2.hire_date,''),nvl(t2.cancel_flag,'')) <> concat(nvl(t3.date_from,''),nvl(t3.hire_date,''),nvl(t3.cancel_flag,''))  
;

3.拉链表和临时表②合并获取临时拉链表,用临时拉链表覆盖拉链表,得到新的拉链表

----拉链表去和每日新增及变化的数据合并
drop table if exists tmp.ems_zipper_tmp;
create table tmp.ems_zipper_tmp as 
select
t1.emp_num     --工号
,t1.curr_org_id       --当前部门组织id
,t1.org_code     --组织名称
,t1.curr_org_name     --组织代码
,t1.hire_date       --入职日期
,t1.date_from       --调入当前网络时间
,t1.cancel_date  --离职日期
,t1.cancel_flag     --离职标识
,t1.zhrlzlx         --离职类型
,t1.type
,from_unixtime(unix_timestamp(case when t1.type = '调岗' then t1.date_from 
when t1.type = '入职' then t1.hire_date
when t1.type = '离职' then t1.cancel_date
end  
,'yyyyMMdd'),'yyyy-MM-dd')    as begin_date 
,'9999-12-30' as end_date
from   tmp.ems_change t1
union all
select
a.emp_num     --工号
,a.curr_org_id       --当前部门组织id
,a.org_code     --组织名称
,a.curr_org_name     --组织代码
,a.hire_date       --入职日期
,a.date_from       --调入当前网络时间
,a.cancel_date     --离职日期
,a.cancel_flag     --离职标识
,a.zhrlzlx         --离职类型
,a.type            --(入职,调岗,离职)
,a.begin_date      --开始日期
,if(b.emp_num is not null and a.end_date = '9999-12-30',case when b.type = '调岗' then date_sub(from_unixtime(unix_timestamp(b.date_from,'yyyyMMdd'),'yyyy-MM-dd'),1) 
when b.type = '离职' then date_sub(from_unixtime(unix_timestamp(b.cancel_date,'yyyyMMdd'),'yyyy-MM-dd'),1) 
when b.type = '入职' then date_sub(from_unixtime(unix_timestamp(b.hire_date,'yyyyMMdd'),'yyyy-MM-dd'),1) 
else '9999-12-30' end ,a.end_date) as end_date   
from ods.ems_zipper a  --拉链表
left join tmp.ems_change b  --新增及其变化表
on a.emp_num = b.emp_num
;
----用临时拉链表覆盖拉链表
insert overwrite table ods.ems_zipper
select 
t1.emp_num   
,t1.curr_org_id 
,t1.org_code 
,t1.curr_org_name  
,t1.hire_date         --入职日期
,t1.date_from         --入网时间
,t1.cancel_date       --离职时间
,t1.cancel_flag       --离职标识
,t1.type              --(入职,调岗,离职)
,t1.zhrlzlx           --离职类型
,t1.begin_date
,t1.end_date
from tmp.ems_zipper_tmp t1
;

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

链表是一种常见的数据结构,它由一系列的结点组成。每个结点包含两部分,即数据域和指针域。数据域用于存储具体的数据,而指针域用于指向下一个结点的地址。链表的基本操作包括插入、删除和查找。 1. 插入操作:链表的插入操作可以在链表的任意位置插入一个新的结点。首先,创建一个新的结点,并将要插入的数据存储在该结点的数据域中。然后,将该结点的指针域指向原来位置的结点,同时修改前一个结点的指针域,使其指向新插入的结点。 2. 删除操作:链表的删除操作可以删除链表中的一个结点。首先,找到要删除的结点,并记录下其前一个结点的地址。然后,将前一个结点的指针域指向要删除结点的下一个结点,从而跳过要删除的结点。 3. 查找操作:链表的查找操作可以在链表中搜索指定的数据。从链表的头结点开始,依次遍历链表的每个结点,直到找到目标数据或者到达链表的末尾。 这些基本操作可以帮助我们进行链表的增删改查操作。通过合理地运用这些操作,我们可以实现各种复杂的功能。需要注意的是,在进行链表操作时,我们需要确保指针的正确性,避免出现指针丢失或者指针指向错误的情况。 总之,链表是一种灵活且常用的数据结构,通过简单的插入、删除和查找操作,我们可以实现各种功能。希望以上解释对您有帮助。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [【C语言链表实现】学生成绩管理系统(功能全面,通俗易懂)](https://blog.csdn.net/weixin_72074975/article/details/130254484)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [C语言链表详解(通俗易懂详细)](https://blog.csdn.net/weixin_46145739/article/details/104590875)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [C语言链表详解(通俗易懂)](https://blog.csdn.net/weixin_61661271/article/details/125131994)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值