ETL之缓慢变化维详解,场景实现

  • 背景

    • 缓慢变化维定义(引自自百度百科)
      维度建模的数据仓库中,有一个概念叫Slowly Changing Dimensions,中文一般翻译成“缓慢变化维”,经常被简写为SCD。缓慢变化维的提出是因为在现实世界中,维度的属性并不是静态的,它会随着时间的流逝发生缓慢的变化。这种随时间发生变化的维度我们一般称之为缓慢变化维,并且把处理维度表的历史变化信息的问题称为处理缓慢变化维的问题,有时也简称为处理SCD的问题。
      处理缓慢变化维的方法通常分为三种方式:
      第一种方式是直接覆盖原值。这样处理,最容易实现,但是没有保留历史数据,无法分析历史变化信息。第一种方式通常简称为“TYPE 1”。
      第二种方式是添加维度行。这样处理,需要代理键的支持。实现方式是当有维度属性发生变化时,生成一条新的维度记录,主键是新分配的代理键,通过自然键可以和原维度记录保持关联。第二种方式通常简称为“TYPE 2”。
      第三种方式是添加属性列。这种处理的实现方式是对于需要分析历史信息的属性添加一列,来记录该属性变化前的值,而本属性字段使用TYPE 1来直接覆盖。这种方式的优点是可以同时分析当前及前一次变化的属性值,缺点是只保留了最后一次变化信息。第三种方式通常简称为“TYPE 3”。
      在实际建模中,我们可以联合使用三种方式,也可以对一个维度表中的不同属性使用不同的方式,这些,都需要根据实际情况来决定,但目的都是一样的,就是能够支持方便的分析历史变化情况。
    • 缓慢变化维举例
      缓慢变化维常用到数仓字典表维度中,一人为例,维度属性很多。
      姓名、性别、身份证、年龄、省份、电话,医保卡、银行卡号等,有很多属性是随着时间变化而变化,这就需要随查随变化,而数仓中,不需要保存这些变化的信息,可以根据关联维度实现缓慢变化维。
      但是如果数据量超大的话,更新的效率太低,这就需要想一些特殊的方法来实现。
  • 解决方案
    在数据库中可以用存储过程、触发器、函数实现。
    但是前两者处理后的数据已经固化,不符合实际的场景,而函数处理完美的解决了这一问题

    • 数据字典
???prompt PL/SQL Developer Export Tables for user C##[email protected]
prompt Created by dongj on 202249set feedback off
set define off

prompt Disabling triggers for DW_PATIENT...
alter table DW_PATIENT disable all triggers;
prompt Deleting DW_PATIENT...
delete from DW_PATIENT;
commit;
prompt Loading DW_PATIENT...
insert into DW_PATIENT (patient_id, birth_date, age)
values ('T00785', to_date('21-12-1948', 'dd-mm-yyyy'), null);
insert into DW_PATIENT (patient_id, birth_date, age)
values ('T01394', to_date('26-08-1978', 'dd-mm-yyyy'), null);
insert into DW_PATIENT (patient_id, birth_date, age)
values ('510102', null, null);
insert into DW_PATIENT (patient_id, birth_date, age)
values ('51010201', null, null);
insert into DW_PATIENT (patient_id, birth_date, age)
values ('T00785', to_date('01-01-2022 13:50:00', 'dd-mm-yyyy hh24:mi:ss'), null);
insert into DW_PATIENT (patient_id, birth_date, age)
values ('T01394', to_date('01-08-1988', 'dd-mm-yyyy'), null);
insert into DW_PATIENT (patient_id, birth_date, age)
values ('T01351', to_date('01-07-1956', 'dd-mm-yyyy'), null);
insert into DW_PATIENT (patient_id, birth_date, age)
values ('T00349', to_date('01-08-1985', 'dd-mm-yyyy'), null);
insert into DW_PATIENT (patient_id, birth_date, age)
values ('T00308', to_date('01-06-1977', 'dd-mm-yyyy'), null);
insert into DW_PATIENT (patient_id, birth_date, age)
values ('T00712', to_date('01-05-2021', 'dd-mm-yyyy'), null);
insert into DW_PATIENT (patient_id, birth_date, age)
values ('T00604', to_date('01-04-2022 21:56:00', 'dd-mm-yyyy hh24:mi:ss'), null);
insert into DW_PATIENT (patient_id, birth_date, age)
values ('T00841', to_date('23-02-1964', 'dd-mm-yyyy'), null);
insert into DW_PATIENT (patient_id, birth_date, age)
values ('T00268', to_date('16-02-2022 00:23:00', 'dd-mm-yyyy hh24:mi:ss'), null);
insert into DW_PATIENT (patient_id, birth_date, age)
values ('T00675', to_date('01-09-1920', 'dd-mm-yyyy'), null);
insert into DW_PATIENT (patient_id, birth_date, age)
values ('510102', null, null);
insert into DW_PATIENT (patient_id, birth_date, age)
values ('51010201', null, null);
commit;
prompt 16 records loaded
prompt Enabling triggers for DW_PATIENT...
alter table DW_PATIENT enable all triggers;

set feedback on
set define on
prompt Done
  • 存储过程实现
CREATE OR REPLACE PROCEDURE P_TO1_PATIENT(O_RETURN OUT VARCHAR2,BRID IN VARCHAR2,PUTDATE  IN DATE) IS
  V_BRID   VARCHAR2(20) := BRID; --病人ID
  V_UPDATE DATE := PUTDATE; --输入更新的时间
  V_CSRQ   DATE; --出生日期
  V_AGE    VARCHAR2(20);
  V_YEAR   VARCHAR2(20);
  V_MONTH  VARCHAR2(20);
  V_DATE   VARCHAR2(20);
  V_HOUR   VARCHAR2(20);
  V_MINUTE VARCHAR2(20);

  TYPE CUR_TYPE IS REF CURSOR;
  CUR_BRXX CUR_TYPE;

BEGIN

  OPEN CUR_BRXX FOR
    SELECT T.PATIENT_ID, T.BIRTH_DATE
      FROM C##DJJ.DW_PATIENT T
     WHERE T.PATIENT_ID = V_BRID;

  LOOP
    FETCH CUR_BRXX
      INTO V_BRID, V_CSRQ;
    EXIT WHEN CUR_BRXX%NOTFOUND;
  
    IF V_CSRQ IS NULL THEN
    
      UPDATE C##DJJ.DW_PATIENT SET AGE = '未知' WHERE PATIENT_ID = V_BRID;
      COMMIT;
    
    ELSE
      --YEAR
      SELECT FLOOR(MONTHS_BETWEEN(V_UPDATE, V_CSRQ) / 12)
        INTO V_YEAR
        FROM DUAL;
      --MONTHS
      SELECT FLOOR(MONTHS_BETWEEN(V_UPDATE, V_CSRQ))
        INTO V_MONTH
        FROM DUAL;
      --DAYS
      SELECT FLOOR(TO_NUMBER(ADD_MONTHS(V_UPDATE, -V_MONTH) - V_CSRQ))
        INTO V_DATE
        FROM DUAL;
      --HOURS
      SELECT FLOOR(TO_NUMBER(V_UPDATE - V_CSRQ) * 24)
        INTO V_HOUR
        FROM DUAL;
      --MINUTES
      SELECT CEIL(TO_NUMBER(V_UPDATE - V_CSRQ) * 24 * 60)
        INTO V_MINUTE
        FROM DUAL;
    
      IF (V_YEAR = 0 AND V_MONTH = 0 AND V_DATE = 0 AND V_HOUR = 0) THEN
        --小于1小时,显示【X分】
        UPDATE C##DJJ.DW_PATIENT
           SET AGE = V_MINUTE || '分'
         WHERE PATIENT_ID = V_BRID;
        COMMIT;
      ELSIF (V_YEAR = 0 AND V_MONTH = 0 AND V_DATE = 0 AND V_HOUR > 0) THEN
        --小于1天,显示【X小时】
        UPDATE C##DJJ.DW_PATIENT
           SET AGE = V_HOUR || '小时'
         WHERE PATIENT_ID = V_BRID;
        COMMIT;
      ELSIF (V_YEAR = 0 AND V_MONTH < 1 AND V_DATE > 0) THEN
        --小于1月,显示【X天】
        UPDATE C##DJJ.DW_PATIENT
           SET AGE = V_DATE || '天'
         WHERE PATIENT_ID = V_BRID;
        COMMIT;
      ELSIF (V_YEAR = 0 AND V_MONTH >= 1 AND V_DATE = 0) THEN
        --小于1岁且天数为0,显示【X月】
        UPDATE C##DJJ.DW_PATIENT
           SET AGE = V_MONTH || '月'
         WHERE PATIENT_ID = V_BRID;
        COMMIT;
      ELSIF (V_YEAR = 0 AND V_MONTH >= 1 AND V_DATE > 0) THEN
        --小于1岁且天数不为0,显示【X月零X天】
        UPDATE C##DJJ.DW_PATIENT
           SET AGE = V_MONTH || '月零' || V_DATE || '天'
         WHERE PATIENT_ID = V_BRID;
        COMMIT;
      END IF;
    
      --小于等于6岁
      IF (V_YEAR > 0 AND V_YEAR <= 6) THEN
        --小于等于6岁且月数为0,显示【X岁】
        IF ((V_MONTH - V_YEAR * 12) = 0) THEN
          UPDATE C##DJJ.DW_PATIENT
             SET AGE = V_YEAR || '岁'
           WHERE PATIENT_ID = V_BRID;
          COMMIT;
        ELSIF ((V_MONTH - V_YEAR * 12) > 0) THEN
          --小于等于6岁且月数不为0,显示【X岁X月】,考虑闰年
          UPDATE C##DJJ.DW_PATIENT
             SET AGE = V_YEAR || '岁' || (V_MONTH - V_YEAR * 12) || '月'
           WHERE PATIENT_ID = V_BRID;
          COMMIT;
        END IF;
      END IF;
    END IF;
  
    --大于6岁显示【X岁】,取整年计算。
    IF (V_YEAR > 6) THEN
      UPDATE C##DJJ.DW_PATIENT
         SET AGE = V_YEAR || '岁'
       WHERE PATIENT_ID = V_BRID;
      COMMIT;
    END IF;
  
  END LOOP;
  CLOSE CUR_BRXX;

  O_RETURN := '0';
EXCEPTION
  WHEN OTHERS THEN
    ROLLBACK;
    O_RETURN := '-1';
    DBMS_OUTPUT.PUT_LINE(SQLERRM);
  
END P_TO1_PATIENT;
  • 触发器实现
CREATE OR REPLACE TRIGGER TRI_DW_PATIENT
  BEFORE INSERT ON C##DJJ.DW_PATIENT
  FOR EACH ROW
DECLARE
  V_YEAR   VARCHAR2(20);
  V_MONTH  VARCHAR2(20);
  V_DATE   VARCHAR2(20);
  V_HOUR   VARCHAR2(20);
  V_MINUTE VARCHAR2(20);
BEGIN

  IF :NEW.BIRTH_DATE IS NULL THEN
    :NEW.AGE := '未知';
  ELSE
    --YEAR
    SELECT FLOOR(MONTHS_BETWEEN(SYSDATE, :NEW.BIRTH_DATE) / 12)
      INTO V_YEAR
      FROM DUAL;
    --MONTHS
    SELECT FLOOR(MONTHS_BETWEEN(SYSDATE, :NEW.BIRTH_DATE))
      INTO V_MONTH
      FROM DUAL;
    --DAYS
    SELECT FLOOR(TO_NUMBER(ADD_MONTHS(SYSDATE, -V_MONTH) - :NEW.BIRTH_DATE))
      INTO V_DATE
      FROM DUAL;
    --HOURS
    SELECT FLOOR(TO_NUMBER(SYSDATE - :NEW.BIRTH_DATE) * 24)
      INTO V_HOUR
      FROM DUAL;
    --MINUTES
    SELECT CEIL(TO_NUMBER(SYSDATE - :NEW.BIRTH_DATE) * 24 * 60)
      INTO V_MINUTE
      FROM DUAL;
  
    IF (V_YEAR = 0 AND V_MONTH = 0 AND V_DATE = 0 AND V_HOUR = 0) THEN
      --小于1小时,显示【X分】
      :NEW.AGE := V_MINUTE || '分';
    ELSIF (V_YEAR = 0 AND V_MONTH = 0 AND V_DATE = 0 AND V_HOUR > 0) THEN
      --小于1天,显示【X小时】
      :NEW.AGE := V_HOUR || '小时';
    ELSIF (V_YEAR = 0 AND V_MONTH < 1 AND V_DATE > 0) THEN
      --小于1月,显示【X天】
      :NEW.AGE := V_DATE || '天';
    ELSIF (V_YEAR = 0 AND V_MONTH >= 1 AND V_DATE = 0) THEN
      --小于1岁且天数为0,显示【X月】
      :NEW.AGE := V_MONTH || '月';
    ELSIF (V_YEAR = 0 AND V_MONTH >= 1 AND V_DATE > 0) THEN
      --小于1岁且天数不为0,显示【X月零X天】
      :NEW.AGE := V_MONTH || '月零' || V_DATE || '天';
    END IF;
  
    --小于等于6岁
    IF (V_YEAR > 0 AND V_YEAR <= 6) THEN
      --小于等于6岁且月数为0,显示【X岁】
      IF ((V_MONTH - V_YEAR * 12) = 0) THEN
        :NEW.AGE := V_YEAR || '岁';
        COMMIT;
      ELSIF ((V_MONTH - V_YEAR * 12) > 0) THEN
        --小于等于6岁且月数不为0,显示【X岁X月】,考虑闰年
        :NEW.AGE := V_YEAR || '岁' || (V_MONTH - V_YEAR * 12) || '月';
      END IF;
    END IF;
  END IF;

  --大于6岁显示【X岁】,取整年计算。
  IF (V_YEAR > 6) THEN
    :NEW.AGE := V_YEAR || '岁';
  END IF;

END;
  • 函数实现
CREATE OR REPLACE FUNCTION FN_PATIENT_AGE(CSRQ IN DATE, JSRQ IN DATE)
  RETURN VARCHAR2 AS
  RESULT     VARCHAR2(64);
  YEARS      NUMBER;
  MONTHS     NUMBER;
  DAYS       NUMBER;
  HOURS      NUMBER;
  ADULTYEARS NUMBER;
  MINUTES    NUMBER;
BEGIN
  --RESULT
  RESULT := '未知';

  -- YEARS
  SELECT FLOOR(MONTHS_BETWEEN(JSRQ, CSRQ) / 12) INTO YEARS FROM DUAL;

  --MONTHS
  SELECT FLOOR(MONTHS_BETWEEN(JSRQ, CSRQ)) INTO MONTHS FROM DUAL;

  --DAYS
  SELECT FLOOR(TO_NUMBER(ADD_MONTHS(JSRQ, -MONTHS) - CSRQ))
    INTO DAYS
    FROM DUAL;

  --HOURS
  SELECT FLOOR(TO_NUMBER(JSRQ - CSRQ) * 24) INTO HOURS FROM DUAL;

  --MINUTES
  SELECT CEIL(TO_NUMBER(JSRQ - CSRQ) * 24 * 60) INTO MINUTES FROM DUAL;

  -- ADULTYEARS
  SELECT TO_CHAR(JSRQ, 'YYYY') - TO_CHAR(CSRQ, 'YYYY')
    INTO ADULTYEARS
    FROM DUAL;

  --小于1小时,显示【X分】
  IF (YEARS = 0 AND MONTHS = 0 AND DAYS = 0 AND HOURS = 0) THEN
    RESULT := MINUTES || '分';
  END IF;

  --小于1天,显示【X小时】
  IF (YEARS = 0 AND MONTHS = 0 AND DAYS = 0 AND HOURS > 0) THEN
    RESULT := HOURS || '小时';
  END IF;

  --小于1月,显示【X天】
  IF (YEARS = 0 AND MONTHS < 1 AND DAYS > 0) THEN
    RESULT := DAYS || '天';
  END IF;

  --小于1岁且天数为0,显示【X月】
  IF (YEARS = 0 AND MONTHS >= 1 AND DAYS = 0) THEN
    RESULT := MONTHS || '月';
  END IF;

  --小于1岁且天数不为0,显示【X月零X天】
  IF (YEARS = 0 AND MONTHS >= 1 AND DAYS > 0) THEN
    RESULT := MONTHS || '月零' || DAYS || '天';
  END IF;

  --小于等于6岁
  IF (YEARS > 0 AND YEARS <= 6) THEN
    --小于等于6岁且月数为0,显示【X岁】
    IF ((MONTHS - YEARS * 12) = 0) THEN
      RESULT := YEARS || '岁';
      --小于等于6岁且月数不为0,显示【X岁X月】,考虑闰年
    ELSE
      IF ((MONTHS - YEARS * 12) > 0) THEN
        RESULT := YEARS || '岁' || (MONTHS - YEARS * 12) || '月';
      END IF;
    END IF;
  END IF;

  --大于6岁显示【X岁】,取整年计算。
  IF (YEARS > 6) THEN
    RESULT := YEARS || '岁';
  END IF;

  RETURN RESULT;
END;

知识不能白嫖,客官点个关注在走吧

你可能感兴趣的:(ETL,oracle,sql,oracle,etl)