1. 程式人生 > >拉鍊表--實現、更新及回滾的具體實現

拉鍊表--實現、更新及回滾的具體實現

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 拉鍊表更新方案

  假設:

    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的資料如下:

  1. 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),刷新歷史表:

先把資料放到一張臨時表中:

  1. 複製程式碼

    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;

 

    重新整理完後,歷史表中資料如下:

  1. 複製程式碼

    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天資料即可)

 

 

參考文章:

  http://lxw1234.com/archives/2015/04/20.htm

    http://lxw1234.com/archives/2015/08/473.htm