历史拉链表是处理增量数据很好的方式,节约了存储空间,也方便历史数据的读取,但实际在做的过程中却也引出了一堆问题。
基础
历史拉链是一种设计方式,关系数据库中,通过开始时间和结束时间标识元组,在这段时间内,元组的其它元素都是不变的。所以如果设计一张关系历史拉链表,开始时间和结束时间理论上必须被包含在主键中,之所以说包含,是因为可能存在其它字段一起构成主键,比如一张用户历史拉链表中,不同的用户历史拉链是不同的,所以用户也包含在主键中。
历史拉链表一般用来搜索在某个时间点的数据信息,比如下表,通过where uid = 1 and start_time <= '2016-10-12' and end_time > '2016-10-12'
就可以拿到用户1在2016-10-12的数据。2016-10-12用户1数据发生变化,插入一条2016-10-12~2030-01-01的数据,然后update table set end_time = '2016-10-12' where uid = 1 and start_time <= '2016-10-11' and end_time > '2016-10-11'
,仔细看where子句是通过前一天查询最后一条数据,这样更新和插入操作可以无序,而且OLAP中数据重跑也可以从出错那天开始。
uid | start_time | end_time | field1 | field2 |
---|---|---|---|---|
1 | 2015-12-12 | 2016-02-16 | 1 | 2 |
1 | 2016-02-16 | 2016-07-14 | 3 | 6 |
1 | 2016-07-14 | 2030-01-01 | 4 | 7 |
2 | 2015-12-12 | 2030-01-01 | 12 | 12 |
注:OLAP中,如果需要重跑某天『设为DATE』的数据,那后面的数据都需要重跑『其实也可开发更新脚本,但如果重跑时间可以接受,就没有必要浪费开发时间』。重跑之前,end_time >= DATE
的所有行end_time都需要更新为极大值『这里为2030-01-01』,start_time >= DATE
的所有行都要被删除。当然,也可以按天操作,即更新end_time = CURR_DATE
的行,删除start_time = CURR_DATE
的行。这样做的目的都是为了还原数据到重跑之前的状态。
注:在关拉链极大值非无穷大或者远大于当前时间值的时候,可以考虑加入标签字段,用来标识最新的一条数据。
事务
OLTP中,如果采用历史拉链表,一定要做事务!
性能
因为每次插入都伴随一次更新操作,所以插入会比较慢。删除则伴随两次更新操作,但一般不会做删除操作,即使做也不做硬删除。OLTP中看具体的场景优化,OLAP则采用挪表的方式解决更新慢的问题,如果是定时数据而且拉数据的时间分片,可以考虑是用分区表。
监控
历史拉链表要做定期监控,因为很容易出错,而且出错了问题很大,尤其是在数据有历史依赖关系的表中,出错了更麻烦。不过有历史依赖的历史拉链表不会也不赞成用在OLTP中,因为在OLAP中如果出错了可能还能重跑,OLTP中出错了那只能手动修了。话说前段时间看了阿里的大数据系统演讲,说流处理的中间结果可以保证100%的正确性,我在想真的假的,真的就太牛逼了。
历史数据
OLAP中这是个麻烦事,尤其是在老系统设计糟糕的情况下,只提一点,历史数据分段跑,跑一段测试一段数据正确性,不然跑完了发现错了一点会哭的。
拉链叠加表和拉链明细表
给定一个需求——得到历史每天每个用户的历史总订单额,如果采用拉链表做,一般有以下两种做法:
- 做拉链叠加表,即每来一笔订单将该用户的最后一条汇总数据取出加上当前订单金额得到总金额,然后作为拉链数据插入。
- 优势:只需一张表,一条简单sql拿到想要用户的任何时间点数据
- 劣势:查错麻烦,一旦某笔订单出错,那从这笔订单后所有数据都要修复更新
- 做拉链明细表,即每来一笔订单只将这笔订单的金额作为一条数据加到拉链表,然后在上层按天和用户聚类加和,最后上层取数据的时候实时加和(2年就是730条数据加和),如果依然慢,则考虑继续按周月年聚类,然后业务层拿数据的时候分析按需整合数据即可。
- 优势:查错方便,修复只需要修复对应聚类段
- 劣势:数据量增加,业务层代码逻辑稍显复杂
为了查错简单,建议第二种方案,因为保证中间结果100%的正确性几乎不可能。