1 背景
本文前面的內容時參考了'lxw的大數據田地',具體可查看最后的'參考文章',個人加入了'拉鏈表的回滾'部分的內容sql,如果有實踐的,可以互相交流學習,謝謝
在數據倉庫的數據模型設計過程中,經常會遇到這樣的需求:
1.1 數據量比較大;
1.2 表中的部分字段會被update,如用戶的地址,產品的描述信息,訂單的狀態等等;
1.3 需要查看某一個時間點或者時間段的歷史快照信息,比如,查看某一個訂單在歷史某一個時間點的狀態,比如,查看某一個用戶在過去某一段時間內,更新過幾次等等;
1.4 變化的比例和頻率不是很大,比如,總共有1000萬的會員,每天新增和發生變化的有10萬左右;如果對這邊表每天都保留一份全量,那么每次全量中會保存很多不變的信息,對存儲是極大的浪費;
綜上所述:引入'拉鏈歷史表',既能滿足反應數據的歷史狀態,又可以最大程度的節省存儲;
2 具體表結構
2.1 例如
有一張訂單表,6月20號有3條記錄:
訂單創建日期 | 訂單編號 | 訂單狀態 |
2012-06-20 | 001 | 創建訂單 |
2012-06-20 | 002 | 創建訂單 |
2012-06-20 | 003 | 支付完成 |
到6月21日,表中有5條記錄:
訂單創建日期 | 訂單編號 | 訂單狀態 |
2012-06-20 | 001 | 支付完成(從創建到支付) |
2012-06-20 | 002 | 創建訂單 |
2012-06-20 | 003 | 支付完成 |
2012-06-21 | 004 | 創建訂單 |
2012-06-21 | 005 | 創建訂單 |
到6月22日,表中有6條記錄:
訂單創建日期 | 訂單編號 | 訂單狀態 |
2012-06-20 | 001 | 支付完成(從創建到支付) |
2012-06-20 | 002 | 創建訂單 |
2012-06-20 | 003 | 已發貨(從支付到發貨) |
2012-06-21 | 004 | 創建訂單 |
2012-06-21 | 005 | 支付完成(從創建到支付) |
2012-06-22 | 006 | 創建訂單 |
2.2 常用的解決方案以及存在的問題:
1 快照表:只保留一份全量,則數據和6月22日的記錄一樣,如果需要查看6月21日訂單001的狀態,則無法滿足;
2 全量歷史表:每天都保留一份全量,則數據倉庫中的該表共有14條記錄,但好多記錄都是重復保存,沒有任務變化,如訂單002,004,數據量大了,會造成很大的存儲浪費;
2.3 如果在數據倉庫中設計成歷史拉鏈表保存該表,則會有下面這樣一張表:
訂單創建日期 | 訂單編號 | 訂單狀態 | dw_begin_date | dw_end_date |
2012-06-20 | 001 | 創建訂單 | 2012-06-20 | 2012-06-20 |
2012-06-20 | 001 | 支付完成 | 2012-06-21 | 9999-12-31 |
2012-06-20 | 002 | 創建訂單 | 2012-06-20 | 9999-12-31 |
2012-06-20 | 003 | 支付完成 | 2012-06-20 | 2012-06-21 |
2012-06-20 | 003 | 已發貨 | 2012-06-22 | 9999-12-31 |
2012-06-21 | 004 | 創建訂單 | 2012-06-21 | 9999-12-31 |
2012-06-21 | 005 | 創建訂單 | 2012-06-21 | 2012-06-21 |
2012-06-21 | 005 | 支付完成 | 2012-06-22 | 9999-12-31 |
2012-06-22 | 006 | 創建訂單 | 2012-06-22 | 9999-12-31 |
說明:
2.3.1 dw_begin_date表示該條記錄的生命周期開始時間(周期快照時的狀態),dw_end_date表示該條記錄的生命周期結束時間;
2.3.2 dw_end_date = '9999-12-31'表示該條記錄目前處於有效狀態;
2.3.3 如果查詢當前所有有效的記錄,則select * from order_his where dw_end_date = '9999-12-31'
2.3.4 如果查詢2012-06-21的歷史快照,則select * from order_his where dw_begin_date <= '2012-06-21' and end_date >='2012-06-21',這條語句會查詢到以下記錄:
訂單創建日期 | 訂單編號 | 訂單狀態 | dw_begin_date | dw_end_date |
2012-06-20 | 001 | 支付完成 | 2012-06-21 | 9999-12-31 |
2012-06-20 | 002 | 創建訂單 | 2012-06-20 | 9999-12-31 |
2012-06-20 | 003 | 支付完成 | 2012-06-20 | 2012-06-21 |
2012-06-21 | 004 | 創建訂單 | 2012-06-21 | 9999-12-31 |
2012-06-21 | 005 | 創建訂單 | 2012-06-21 | 2012-06-21 |
和源表在6月21日的記錄完全一致:
訂單創建日期 | 訂單編號 | 訂單狀態 |
2012-06-20 | 001 | 支付完成(從創建到支付) |
2012-06-20 | 002 | 創建訂單 |
2012-06-20 | 003 | 支付完成 |
2012-06-21 | 004 | 創建訂單 |
2012-06-21 | 005 | 創建訂單 |
可以看出,這樣的歷史拉鏈表,既能滿足對歷史數據的需求,又能很大程度的節省存儲資源;
3.1 前提:
3.1.1 數據倉庫中訂單歷史表的刷新頻率為一天,當天更新前一天的增量數據;
3.1.2 如果一個訂單在一天內有多次狀態變化,則只會記錄最后一個狀態的歷史;
3.1.3 訂單狀態包括三個:創建、支付、完成;
3.1.4 創建時間和修改時間只取到天,如果源訂單表中沒有狀態修改時間,那么抽取增量就比較麻煩,需要有個機制來確保能抽取到每天的增量數據;
-- 例如DB中的binlog解析,或者通過sqoop同步,只同步有過修改的數據(新增 or 修改)
3.1.5 本文中的表和SQL都使用Hive的HQL語法;
3.1.6 源系統中訂單表結構為:
CREATE TABLE orders ( orderid INT, createtime STRING, modifiedtime STRING, status STRING ) stored AS textfile;
3.2 在數據倉庫的ODS層,有一張訂單的增量數據表,按天分區,存放每天的增量數據:
CREATE TABLE t_ods_orders_inc ( orderid INT, createtime STRING, modifiedtime STRING, status STRING ) PARTITIONED BY (day STRING) stored AS textfile;
3.3 在數據倉庫的DW層,有一張訂單的歷史數據拉鏈表,存放訂單的歷史狀態數據:
CREATE TABLE t_dw_orders_his ( orderid INT, createtime STRING, modifiedtime STRING, status STRING, dw_start_date STRING, dw_end_date STRING ) stored AS textfile;
3.4 2015-08-21至2015-08-23,每天原系統訂單表的數據如下,紅色標出的為當天發生變化的訂單,即增量數據:
3.5 具體步驟:
在數據從源業務系統每天正常抽取和刷新到DW訂單歷史表之前,需要做一次全量的初始化,就是從源訂單表中昨天以前的數據全部抽取到ODW,並刷新到DW。
以上面的數據為例,比如在2015-08-21這天做全量初始化,那么我需要將包括2015-08-20之前的所有的數據都抽取並刷新到DW:
3.5.1 抽取全量數據到ODS:
INSERT overwrite TABLE t_ods_orders_inc PARTITION (day = ‘2015-08-20′) SELECT orderid,createtime,modifiedtime,status FROM orders WHERE createtime <= ‘2015-08-20′;
3.5.2 從ODS刷新到DW:
INSERT overwrite TABLE t_dw_orders_his SELECT orderid,createtime,modifiedtime,status, createtime AS dw_start_date, ‘9999-12-31′ AS dw_end_date FROM t_ods_orders_inc WHERE day = ‘2015-08-20′;
完成后,DW訂單歷史表中數據如下:
spark-sql> select * from t_dw_orders_his; 1 2015-08-18 2015-08-18 創建 2015-08-18 9999-12-31 2 2015-08-18 2015-08-18 創建 2015-08-18 9999-12-31 3 2015-08-19 2015-08-21 支付 2015-08-19 9999-12-31 4 2015-08-19 2015-08-21 完成 2015-08-19 9999-12-31 5 2015-08-19 2015-08-20 支付 2015-08-19 9999-12-31 6 2015-08-20 2015-08-20 創建 2015-08-20 9999-12-31 7 2015-08-20 2015-08-21 支付 2015-08-20 9999-12-31 Time taken: 2.296 seconds, Fetched 7 row(s)
3.5.3 增量抽取
每天,從源系統訂單表中,將前一天的增量數據抽取到ODS層的增量數據表。
這里的增量需要通過訂單表中的創建時間和修改時間來確定:
INSERT overwrite TABLE t_ods_orders_inc PARTITION (day = '${day}') SELECT orderid,createtime,modifiedtime,status FROM orders WHERE createtime = '${day}' OR modifiedtime = '${day}';
注意:在ODS層按天分區的增量表,最好保留一段時間的數據,比如半年,為了防止某一天的數據有問題而回滾重做數據。
3.5.4 增量刷新歷史數據
從2015-08-22開始,需要每天正常刷新前一天(2015-08-21)的增量數據到歷史表。
3.5.4.1 通過增量抽取,將2015-08-21的數據抽取到ODS:
INSERT overwrite TABLE t_ods_orders_inc PARTITION (day = '2015-08-21') SELECT orderid,createtime,modifiedtime,status FROM orders WHERE createtime = '2015-08-21' OR modifiedtime = '2015-08-21';
ODS增量表中2015-08-21的數據如下:
-
spark-sql> select * from t_ods_orders_inc where day = '2015-08-21'; 3 2015-08-19 2015-08-21 支付 2015-08-21 4 2015-08-19 2015-08-21 完成 2015-08-21 7 2015-08-20 2015-08-21 支付 2015-08-21 8 2015-08-21 2015-08-21 創建 2015-08-21 Time taken: 0.437 seconds, Fetched 4 row(s)
3.5.4.2 通過DW歷史數據(數據日期為2015-08-20),和ODS增量數據(2015-08-21),刷新歷史表:
先把數據放到一張臨時表中:
-
DROP TABLE IF EXISTS t_dw_orders_his_tmp; CREATE TABLE t_dw_orders_his_tmp
AS SELECT
orderid, createtime, modifiedtime, status, dw_start_date, dw_end_date FROM
(SELECT
a.orderid, a.createtime, a.modifiedtime, a.status, a.dw_start_date, CASE WHEN b.orderid IS NOT NULL AND a.dw_end_date > '2015-08-21' THEN '2015-08-20' ELSE a.dw_end_date END AS dw_end_date FROM t_dw_orders_his a left outer join (SELECT * FROM t_ods_orders_inc WHERE day = '2015-08-21') b ON (a.orderid = b.orderid)
UNION ALL
SELECT
orderid, createtime, modifiedtime, status, modifiedtime AS dw_start_date, '9999-12-31' AS dw_end_date FROM t_ods_orders_inc WHERE day = '2015-08-21' ) x ORDER BY orderid,dw_start_date;其中:
UNION ALL的兩個結果集中,第一個是用歷史表left outer join 日期為 ${yyy-MM-dd} 的增量,能關聯上的,並且dw_end_date > ${yyy-MM-dd},說明狀態有變化,則把原來的dw_end_date置為(${yyy-MM-dd} – 1), 關聯不上的,說明狀態無變化,dw_end_date無變化。
第二個結果集是直接將增量數據插入歷史表。
3.5.5 最后把臨時表中數據插入歷史表:
INSERT overwrite TABLE t_dw_orders_his SELECT * FROM t_dw_orders_his_tmp;
刷新完后,歷史表中數據如下:
-
spark-sql> select * from t_dw_orders_his order by orderid,dw_start_date; 1 2015-08-18 2015-08-18 創建 2015-08-18 9999-12-31 2 2015-08-18 2015-08-18 創建 2015-08-18 9999-12-31 3 2015-08-19 2015-08-21 支付 2015-08-19 2015-08-20 3 2015-08-19 2015-08-21 支付 2015-08-21 9999-12-31 4 2015-08-19 2015-08-21 完成 2015-08-19 2015-08-20 4 2015-08-19 2015-08-21 完成 2015-08-21 9999-12-31 5 2015-08-19 2015-08-20 支付 2015-08-19 9999-12-31 6 2015-08-20 2015-08-20 創建 2015-08-20 9999-12-31 7 2015-08-20 2015-08-21 支付 2015-08-20 2015-08-20 7 2015-08-20 2015-08-21 支付 2015-08-21 9999-12-31 8 2015-08-21 2015-08-21 創建 2015-08-21 9999-12-31 Time taken: 0.717 seconds, Fetched 11 row(s)
由於在2015-08-21做了8月20日以前的數據全量初始化,而訂單3、4、7在2015-08-21的增量數據中也存在,因此都有兩條記錄,但不影響后面的查詢。
4 拉鏈表回滾
4.1 具體操作方案
假設恢復到t天之前的數據,即未融合t天數據之前的拉鏈表,假設標記的開始日期和結束日期分別為s、t,具體分析如下:
1 當t-1>e時,s數據、e數據在t天之前產生,保留即可 2 當t-1=e時,e數據在t天產生,需修改 3 當s<t<=e時,e數據在t+n天產生,需修改 4 當s>=t時,s數據、e數據在t+n天產生,刪除即可
具體例子:
spark-sql> select * from t_dw_orders_his order by orderid,dw_start_date; 1 2015-08-18 2015-08-18 創建 2015-08-18 2015-08-21 1 2015-08-18 2015-08-22 支付 2015-08-22 2015-08-22 1 2015-08-18 2015-08-23 完成 2015-08-23 9999-12-31 2 2015-08-18 2015-08-18 創建 2015-08-18 2015-08-21 2 2015-08-18 2015-08-22 完成 2015-08-22 9999-12-31 3 2015-08-19 2015-08-21 支付 2015-08-19 2015-08-20 3 2015-08-19 2015-08-21 支付 2015-08-21 2015-08-22 3 2015-08-19 2015-08-23 完成 2015-08-23 9999-12-31 4 2015-08-19 2015-08-21 完成 2015-08-19 2015-08-20 4 2015-08-19 2015-08-21 完成 2015-08-21 9999-12-31 5 2015-08-19 2015-08-20 支付 2015-08-19 2015-08-22 5 2015-08-19 2015-08-23 完成 2015-08-23 9999-12-31 6 2015-08-20 2015-08-20 創建 2015-08-20 2015-08-21 6 2015-08-20 2015-08-22 支付 2015-08-22 9999-12-31 7 2015-08-20 2015-08-21 支付 2015-08-20 2015-08-20 7 2015-08-20 2015-08-21 支付 2015-08-21 9999-12-31 8 2015-08-21 2015-08-21 創建 2015-08-21 2015-08-21 8 2015-08-21 2015-08-22 支付 2015-08-22 2015-08-22 8 2015-08-21 2015-08-23 完成 2015-08-23 9999-12-31 9 2015-08-22 2015-08-22 創建 2015-08-22 9999-12-31 10 2015-08-22 2015-08-22 支付 2015-08-22 9999-12-31 11 2015-08-23 2015-08-23 創建 2015-08-23 9999-12-31 12 2015-08-23 2015-08-23 創建 2015-08-23 9999-12-31 13 2015-08-23 2015-08-23 支付 2015-08-23 9999-12-31
比如在插入2015-08-23的數據后,回滾2015-08-22的數據,使拉鏈表與2015-08-21的一致,具體操作過程如下
1 增加臨時表t_dw_orders_his_tmp1,用來記錄t-1>e的數據 CREATE TABLE t_dw_orders_his_tmp1 AS SELECT orderid, createtime, modifiedtime, status, dw_start_date, dw_end_date FROM
t_dw_orders_his WHERE
dw_end_date < '2015-08-21'
3 2015-08-19 2015-08-21 支付 2015-08-19 2015-08-20 4 2015-08-19 2015-08-21 完成 2015-08-19 2015-08-20 7 2015-08-20 2015-08-21 支付 2015-08-20 2015-08-20
2 增加臨時表t_dw_orders_his_tmp2,用來記錄t-1=e的數據
CREATE TABLE t_dw_orders_his_tmp2
AS
SELECT
orderid,
createtime,
modifiedtime,
status,
dw_start_date,
'9999-12-31' AS dw_end_date
FROM
t_dw_orders_his WHERE
dw_end_date = '2015-08-21'
1 2015-08-18 2015-08-18 創建 2015-08-18 9999-12-31 2 2015-08-18 2015-08-18 創建 2015-08-18 9999-12-31 6 2015-08-20 2015-08-20 創建 2015-08-20 9999-12-31 8 2015-08-21 2015-08-21 創建 2015-08-21 9999-12-31
3 增加臨時表t_dw_orders_his_tmp3,用來記錄s<t<=e的數據 CREATE TABLE t_dw_orders_his_tmp3 AS SELECT orderid, createtime, modifiedtime, status, dw_start_date, '9999-12-31' dw_end_date FROM
t_dw_orders_his WHERE
dw_start_date < '2015-08-22' AND dw_end_date >= '2015-08-22'
3 2015-08-19 2015-08-21 支付 2015-08-21 9999-12-31 4 2015-08-19 2015-08-21 完成 2015-08-21 9999-12-31 5 2015-08-19 2015-08-20 支付 2015-08-19 9999-12-31 7 2015-08-20 2015-08-21 支付 2015-08-21 9999-12-31
4 所有數據插入新表t_dw_orders_his_new CREATE TABLE t_dw_orders_his_new AS SELECT * FROM t_dw_orders_his_tmp1 UNION ALL SELECT * FROM t_dw_orders_his_tmp2 UNION ALL SELECT * FROM t_dw_orders_his_tmp3
1 2015-08-18 2015-08-18 創建 2015-08-18 9999-12-31 2 2015-08-18 2015-08-18 創建 2015-08-18 9999-12-31
3 2015-08-19 2015-08-21 支付 2015-08-19 2015-08-20
3 2015-08-19 2015-08-21 支付 2015-08-21 9999-12-31
4 2015-08-19 2015-08-21 完成 2015-08-19 2015-08-20
4 2015-08-19 2015-08-21 完成 2015-08-21 9999-12-31
5 2015-08-19 2015-08-20 支付 2015-08-19 9999-12-31
6 2015-08-20 2015-08-20 創建 2015-08-20 9999-12-31
7 2015-08-20 2015-08-21 支付 2015-08-20 2015-08-20
7 2015-08-20 2015-08-21 支付 2015-08-21 9999-12-31
8 2015-08-21 2015-08-21 創建 2015-08-21 9999-12-31
與原數據一致,驗證無錯
4.2 備用方案
可以采用備份的方案,保證無誤和可行。(保存增量數據,並對t_dw_orders_his表每個月備份一次全量數據。如需回滾,最多重跑30天數據即可)