數據倉庫@緩慢變化維(拉鍊算法)

前言

維度表中的數據來源於操作型系統。在多維數據倉庫或獨立型數據集市中,數據直接來源於操作型系統。在企業信息化工廠中,來自於操作型系統的數據首先移到企業數據倉庫中,然後進入多維數據集市。進入到維度表的信息,在操作型系統中仍然有可能發生改變。例如:客戶的生日出現錯誤可能需要更新以糾正,客戶的地址發生變化也需要更新等。

由於下游的星型模式使用代理鍵作爲每個維度表的主鍵,因此不需要像原系統那樣處理信息變化。操作型系統可以跟蹤數據變化的歷史情況,也可以簡單的採用重寫變化值的方式。無論如何,星型模式可以對不同方式的變化做出響應,這使得對整個業務過程的度量更有意義。

採用維度設計方案時,確定數據源的變化情況在維度表中如何表示非常重要。這一現象稱爲緩慢變化的維度,簡稱緩慢變化維(Slowly Changing Dimension)。該術語的名稱反映了維度積累變化的實際情況,至少與積累數據行較爲快速的事實表比較,維度變化相對緩慢。應對數據元素改變存在不同的響應方式。某些情況下,保留歷史數據沒有什麼分析價值。某些情況下,保留歷史數據將會起到至關重要的作用。——以上摘自數據倉庫維度設計權威指南

 

說明

緩慢變化維其實在Kimball的維度數據倉庫構建過程中是經常使用到的,所以掌握這樣一個基本算法也是在做數據倉庫開發工作中的必備技能,當然有時候我們也會聽說拉鍊算法(俗稱)其實都是指的是同一個東西

 

源業務系統中客戶表

在源業務系統中的數據表是隨時發生變化並且不會記錄歷史,只需要給客戶呈現最新的數據即可

客戶號 姓名 地址 電話
CIF10001 Jack China 17711111111
CIF10002 Rose China 17722222222

 

實現方式一:每日快照

可以看出客戶Rose在20190103這天修改了手機號,那麼我們只需要將源業務系統的數據表追加一個數據日期並進行每天全量快照就可以記錄下數據每天的歷史變化,一遍我們後續的維度分析。但是很明顯這有一個缺點,就是數據重複非常嚴重,那麼我們如果實現最少數據重複並記錄數據的歷史變化呢?繼續看方式二

數據日期 客戶號 姓名 地址 電話
20190101 CIF10001 Jack China 17711111111
20190101 CIF10002 Rose China

17722222222

20190102 CIF10001 Jack China 17711111111
20190102 CIF10002 Rose China

17722222222

20190103 CIF10001 Jack China 17711111111
20190103 CIF10002 Rose China 17733333333

 

實現方式二:歷史拉鍊

同樣是反映歷史數據變化,我們的拉鍊方式明顯可以看出數據量比方式一少了很多冗餘,所以這種方式是推崇的。在客戶Rose20190103這天做了手機號的一個變更我的拉鍊算法就會根據數據的變化進行記錄,巧妙的運用了數據的生命週期進行管理。開始時間和結束時間這個時間區間內表明數據是有效的,從而減少了大量不變數據的數據冗餘,也就是說我們這種算法是推薦用於不是大面積的數據變化,而是小範圍的數據變更才更具有優勢,因爲是用時間換取了空間。從而也說明了方式一,是空間換取了時間。

開始日期 結束日期 客戶號 姓名 地址 電話
20190101 99991231 CIF10001 Jack China 17711111111
20190101 20190102 CIF10002 Rose China

17722222222

20190103 99991231 CIF10002 Rose China

17733333333

 

取數邏輯實現

既然數據已經存好了,那麼我們如何進行取數呢?方式一的取數邏輯已經很明顯,只需要按照數據日期進行過濾即可,但是方式二呢?那麼就讓我們一起分析一下它的取數邏輯實現,我們現在就是要充分利用數據的生命週期來反映數據的歷史變化。

獲取20190101歷史那天的數據那麼我們的SQL可以寫成:

Select * From User_Info Where '20190101' between Start_Date and End_Date;

獲取20190102歷史那天的數據那麼我們的SQL可以寫成:

Select * From User_Info Where '20190102' between Start_Date and End_Date;

獲取20190103歷史那天的數據那麼我們的SQL可以寫成:

Select * From User_Info Where '20190103' between Start_Date and End_Date;

獲取最新的數據那麼我們的SQL可以寫成:

Select * From User_Info Where End_Date = ‘99991231’;

方式一 方式二
Where Data_Date = SomeDay Where SomeDay between Start_Date and End_Date

 

算法實現

其實,原理上面已經講的很清楚了,針對不同的數據庫具體的實現也是不太一樣的,那麼我這裏給出一個MySQL的版本,供大家參考,Oracle的實現其實是比MySQL簡單的

DELIMITER $$

DROP PROCEDURE IF EXISTS ETL.EDW_SCD_LOAD$$

CREATE PROCEDURE ETL.EDW_SCD_LOAD(
     IN P_DATA_DATE VARCHAR(50)
    ,IN P_IN_SCHEMA VARCHAR(50)
    ,IN P_IN_TABLE  VARCHAR(50)
    ,IN P_TO_SCHEMA VARCHAR(50)
    ,IN P_TO_TABLE  VARCHAR(50)
    ,OUT P_RESULT   INT
    )
    /*LANGUAGE SQL
    | [NOT] DETERMINISTIC
    | { CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA }
    | SQL SECURITY { DEFINER | INVOKER }
    | COMMENT 'string'*/
BEGIN
    /*[DEFINER = { user | CURRENT_USER }]*/
/*
     Author    : XUEZHOUYI
     Name      : EDW_SCD_LOAD
     Functions : Slowly Changing Dimensions
     Purpose   : Slowly Changing Dimensions
     Revisions or Comments
     VER        DATE        AUTHOR           DESCRIPTION
    ---------  ----------  ---------------  ------------------------------------
    1.0        2017-08-01  XUEZHOUYI        1.CREATE THE PROCEDURE
    1.1        2017-08-28  XUEZHOUYI        1.ADD SCHEMAS
*/
    DECLARE V_PROC_NAME     VARCHAR(80)   DEFAULT 'ETL.EDW_SCD_LOAD.PRC';
    DECLARE V_START_TIME    CHAR(19)      DEFAULT NOW();
    DECLARE V_STEP_ID       INT           DEFAULT 0;
    /* ------------------------------------------------------------------------ */
    
    DECLARE V_SQL_STR      VARCHAR(20000) DEFAULT '';
    DECLARE V_COLUMN       VARCHAR(2000)  DEFAULT '';
    DECLARE V_JOIN_KEY     VARCHAR(50)    DEFAULT '';
    DECLARE V_COLUMNS      VARCHAR(2000)  DEFAULT '';
    DECLARE V_AAA_COLUMNS  VARCHAR(2000)  DEFAULT '';
    DECLARE V_JOIN_COLUMNS VARCHAR(2000)  DEFAULT '';
    
    /* DEFINE THE CURSOR */
    DECLARE IF_DONE INT DEFAULT FALSE;
    DECLARE MY_CURSOR1 CURSOR FOR SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = P_IN_SCHEMA AND TABLE_NAME = P_IN_TABLE;
    DECLARE MY_CURSOR2 CURSOR FOR SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = P_IN_SCHEMA AND TABLE_NAME = P_IN_TABLE AND COLUMN_KEY <> 'PRI';
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET IF_DONE = TRUE;
    
    /* EXCEPTION HANDLER  */
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        GET DIAGNOSTICS CONDITION 1 @V_RETURN_CODE = RETURNED_SQLSTATE ,@V_ERROR_MSG = MESSAGE_TEXT;
        CALL ETL.EDW_PROC_ERROR_LOG(P_DATA_DATE,V_START_TIME,NOW(),V_PROC_NAME,V_STEP_ID,@V_RETURN_CODE,@V_ERROR_MSG);
        SET P_RESULT = 1;
    END;
    
    SET P_RESULT = 0;
    /* EXCEPTION HANDLER  */
    
    SET V_STEP_ID = 1;
    /* GET PRIMARY KEY */
    SELECT COLUMN_NAME INTO V_JOIN_KEY FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = P_IN_SCHEMA AND TABLE_NAME = P_IN_TABLE AND COLUMN_KEY = 'PRI';
    
    SET V_STEP_ID = 2;
    /* LOOP */
    OPEN MY_CURSOR1;
        MY_LOOP1:LOOP
            FETCH MY_CURSOR1 INTO V_COLUMN;
            IF IF_DONE THEN
                LEAVE MY_LOOP1;
            END IF;
            
            SET V_COLUMNS = CONCAT(V_COLUMNS,',',V_COLUMN);
            
        END LOOP MY_LOOP1;
    CLOSE MY_CURSOR1;
    
    SET V_STEP_ID = 3;
    SET IF_DONE = FALSE;
    OPEN MY_CURSOR1;
        MY_LOOP1:LOOP
            FETCH MY_CURSOR1 INTO V_COLUMN;
            IF IF_DONE THEN
                LEAVE MY_LOOP1;
            END IF;
            
            SET V_AAA_COLUMNS = CONCAT(V_AAA_COLUMNS,',AAA.',V_COLUMN);
            
        END LOOP MY_LOOP1;
    CLOSE MY_CURSOR1;
    
    SET V_STEP_ID = 4;
    SET IF_DONE = FALSE;
    OPEN MY_CURSOR2;
        MY_LOOP2:LOOP
            FETCH MY_CURSOR2 INTO V_COLUMN;
            IF IF_DONE THEN
                LEAVE MY_LOOP2;
            END IF;
            
            SET V_JOIN_COLUMNS = CONCAT(V_JOIN_COLUMNS,' OR AAA.',V_COLUMN,' <> BBB.',V_COLUMN);
            
        END LOOP MY_LOOP2;
    CLOSE MY_CURSOR2;
    
    SET V_STEP_ID = 5;
    /* SUPPORT FOR RERUN */
    SET @V_SQL_STR = CONCAT('
        UPDATE ',P_TO_SCHEMA,'.',P_TO_TABLE,' SET END_DATE = ',P_DATA_DATE,' WHERE END_DATE = DATE_FORMAT(DATE_SUB(STR_TO_DATE(',P_DATA_DATE,',''%Y%m%d''),INTERVAL 1 DAY),''%Y%m%d'')
    ');
    PREPARE V_SQL_STR FROM @V_SQL_STR;
    EXECUTE V_SQL_STR;
    COMMIT;
    
    SET @V_SQL_STR = CONCAT('
        DELETE FROM ',P_TO_SCHEMA,'.',P_TO_TABLE,' WHERE START_DATE = ',P_DATA_DATE,'
    ');
    PREPARE V_SQL_STR FROM @V_SQL_STR;
    EXECUTE V_SQL_STR;
    COMMIT;
    
    SET V_STEP_ID = 6;
    /* CLOSED THE RECORDS WERE NOT FOUND */
    SET @V_SQL_STR = CONCAT('
        UPDATE ',P_TO_SCHEMA,'.',P_TO_TABLE,' TGT INNER JOIN(
            SELECT
                AAA.',V_JOIN_KEY,'
            FROM
                ',P_TO_SCHEMA,'.',P_TO_TABLE,' AAA
            LEFT JOIN
                ',P_IN_SCHEMA,'.',P_IN_TABLE,' BBB
            ON
                AAA.',V_JOIN_KEY,' = BBB.',V_JOIN_KEY,'
            WHERE
                ',P_DATA_DATE,' BETWEEN AAA.START_DATE AND AAA.END_DATE
            AND BBB.',V_JOIN_KEY,' IS NULL
        ) SRC
        ON TGT.',V_JOIN_KEY,' = SRC.',V_JOIN_KEY,' AND ',P_DATA_DATE,' BETWEEN TGT.START_DATE AND TGT.END_DATE
        SET TGT.END_DATE = DATE_FORMAT(DATE_SUB(STR_TO_DATE(',P_DATA_DATE,',''%Y%m%d''),INTERVAL 1 DAY),''%Y%m%d'')
    ');
    PREPARE V_SQL_STR FROM @V_SQL_STR;
    EXECUTE V_SQL_STR;
    COMMIT;
    
    SET V_STEP_ID = 7;
    /* CLOSED THE RECORDS WERE OUT OF DATE */
    SET @V_SQL_STR = CONCAT('
        UPDATE ',P_TO_SCHEMA,'.',P_TO_TABLE,' TGT INNER JOIN(
            SELECT
                AAA.',V_JOIN_KEY,'
            FROM
                ',P_TO_SCHEMA,'.',P_TO_TABLE,' AAA
            INNER JOIN
                ',P_IN_SCHEMA,'.',P_IN_TABLE,' BBB
            ON
                AAA.',V_JOIN_KEY,' = BBB.',V_JOIN_KEY,'
            AND (1<>1',V_JOIN_COLUMNS,')
            WHERE
                ',P_DATA_DATE,' BETWEEN AAA.START_DATE AND AAA.END_DATE
        ) SRC
        ON TGT.',V_JOIN_KEY,' = SRC.',V_JOIN_KEY,' AND ',P_DATA_DATE,' BETWEEN TGT.START_DATE AND TGT.END_DATE
        SET TGT.END_DATE = DATE_FORMAT(DATE_SUB(STR_TO_DATE(',P_DATA_DATE,',''%Y%m%d''),INTERVAL 1 DAY),''%Y%m%d'')
    ');
    PREPARE V_SQL_STR FROM @V_SQL_STR;
    EXECUTE V_SQL_STR;
    COMMIT;
    
    SET V_STEP_ID = 8;
    /* INSERT THE NEW RECORDS */
    SET @V_SQL_STR = CONCAT('
        INSERT INTO ',P_TO_SCHEMA,'.',P_TO_TABLE,'(START_DATE,END_DATE',V_COLUMNS,')
        SELECT *
        FROM(
            SELECT 
                ',P_DATA_DATE,',99991231',V_AAA_COLUMNS,'
            FROM
                ',P_IN_SCHEMA,'.',P_IN_TABLE,' AAA
            LEFT JOIN
                ',P_TO_SCHEMA,'.',P_TO_TABLE,' BBB
            ON
                AAA.',V_JOIN_KEY,' = BBB.',V_JOIN_KEY,'
            AND ',P_DATA_DATE,' BETWEEN BBB.START_DATE AND BBB.END_DATE
            WHERE
                BBB.',V_JOIN_KEY,' IS NULL
        ) AS TMP
    ');
    PREPARE V_SQL_STR FROM @V_SQL_STR;
    EXECUTE V_SQL_STR;
    COMMIT;

END$$

DELIMITER ;

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章