데이터 웨어하우스 지퍼 테이블 설계 및 구현

1. 소개

  • 증분 테이블: 증분 데이터, 즉 새로운 증분 및 변경 사항을 저장하는 날짜 파티션이 있습니다.
  • 풀 스케일 테이블: 날짜 분할 없음(매일 덮어쓰기 및 업데이트), 현재 시점의 최신 데이터 상태를 저장하므로 데이터의 과거 변경 사항을 기록할 수 없음
  • 스냅샷 테이블 : 날짜 파티션이 있고, 매일 데이터가 가득찬다.(변동 유무와 관계없이) 파티션마다 중복 데이터가 많아 저장 공간이 낭비되는 단점이 있다.
  • 지퍼 테이블: 지퍼 테이블은 과거 상태와 최신 상태 데이터를 유지하는 데 사용되는 테이블입니다.다른 지퍼 입도에 따라 지퍼 테이블은 실제로 스냅샷과 동일하지만 변경되지 않은 일부 레코드를 제거하도록 최적화되었습니다.

2. 적용 시나리오

지퍼 테이블은 많은 양의 데이터가 있고 필드 변경의 비율과 빈도가 적을과거 스냅샷 정보를 확인해야 하는 시나리오에 적합합니다 .

예를 들어, 수천만 개의 레코드와 수백 개의 필드가 있는 고객 테이블이 있습니다. 따라서 이런 종류의 테이블은 ORC 압축을 사용하더라도 단일 테이블의 데이터 저장 공간이 하루에 50GB를 초과하게 되며, HDFS에서 3개의 백업을 사용하는 경우 저장 공간은 더욱 커집니다.

그렇다면 이 테이블을 어떻게 디자인해야 할까요? 다음은 몇 가지 옵션입니다.

  1. 옵션 1(풀스케일 테이블): 매일 최신 데이터를 추출하여 전날의 데이터를 덮어쓰기 구현이 간편하고 공간이 절약된다는 장점이 있지만 단점도 명백하고 히스토리 상태가 없음
  2. 해결방법 2(스냅샷 테이블): 매일 풀로 만들면 과거 데이터를 볼 수 있지만 단점은 저장 공간이 너무 커서 특히 고객 정보가 자주 바뀌지 않는 경우 필드의 반복 저장 비율이 너무 높다는 것입니다.
  3. 솔루션 3(지퍼 테이블): 지퍼 테이블 디자인을 채택하면 이력 조회가 가능할 뿐만 아니라 저장 공간 사용량이 매우 적다(결국 변경되지 않은 데이터는 반복 저장되지 않음)

3. 하이브 SQL 실습

먼저 테스트를 위해 원본 고객 정보 테이블을 만듭니다.

CREATE TABLE IF NOT EXISTS datadev.zipper_table_test_cust_src (
	`cust_id` STRING COMMENT '客户编号',
	`phone` STRING COMMENT '手机号码'
)PARTITIONED BY (
  dt STRING COMMENT 'etldate'
)STORED AS ORC
TBLPROPERTIES ("orc.compress"="SNAPPY")
;

그런 다음 일부 테스트 데이터를 삽입하십시오.

cust_id 핸드폰 dt
001 1111 20210601
002 2222 20210601
003 3333 20210601
004 4444 20210601
001 1111 20210602
002 2222-1 20210602
003 3333 20210602
004 4444-1 20210602
005 5555 20210602
001 1111-1 20210603
002 2222-2 20210603
003 3333 20210603
004 4444-1 20210603
005 5555-1 20210603
006 6666 20210603
002 2222-3 20210604
003 3333 20210604
004 4444-1 20210604
005 5555-1 20210604
006 6666 20210604
007 7777 20210604

데이터에 대한 간략한 설명은 다음과 같습니다.

  • 20210601은 시작일이며 총 4명의 고객이 있습니다.
  • 20210602 002 및 004 고객 정보 업데이트 및 005 고객 추가
  • 20210603 001, 002, 005 고객 정보 업데이트 및 006 고객 추가
  • 20210604 고객 002의 정보 업데이트, 고객 007 추가, 고객 001 삭제

이제 주제로 돌아가서 지퍼 테이블을 디자인하는 방법은 무엇입니까?

우선 지퍼 테이블에는 데이터 유효 날짜데이터 만료 날짜 라는 두 가지 중요한 감사 필드가 있습니다 . 이름에서 알 수 있듯이 데이터 유효일은 레코드가 유효하게 된 시점을 기록하고 데이터 만료일은 레코드의 만료 시간을 기록합니다(9999-12-31은 지금까지 유효했음을 의미). 그런 다음 데이터에 대한 작업을 다음 범주로 나눌 수 있습니다.

  1. 새로 추가된 레코드: 데이터 유효 날짜는 오늘이고 만료 날짜는 9999-12-31입니다.
  2. 변경되지 않은 기록: 데이터의 유효 날짜는 이전에 사용되어야 하며 만료 날짜는 변경되지 않습니다.
  3. 변경 사항이 있는 레코드: == "오래된 레코드의 경우: 유지하고 만료 날짜를 오늘로 변경; == "새 레코드의 경우: 추가, 유효 날짜는 오늘이고 만료 날짜는 9999-12-31입니다.
  4. 삭제된 기록: 루프를 닫아야 하며 만료 날짜가 같은 날이 됩니다.

따라서 지퍼 테이블의 HQL 구현 코드는 다음과 같다.

-- 拉链表建表语句
CREATE TABLE IF NOT EXISTS datadev.zipper_table_test_cust_dst (
  `cust_id` STRING COMMENT '客户编号',
  `phone` STRING COMMENT '手机号码',
  `s_date` DATE COMMENT '生效时间',
  `e_date` DATE COMMENT '失效时间'
)STORED AS ORC
TBLPROPERTIES ("orc.compress"="SNAPPY")
;
-- 拉链表实现代码(含数据回滚刷新)
INSERT OVERWRITE TABLE datadev.zipper_table_test_cust_dst
-- part1: 处理新增的、没有变化的记录,以及有变化的记录中的新记录
select NVL(curr.cust_id, prev.cust_id) as cust_id,
       NVL(curr.phone, prev.phone) as phone,
       -- 没有变化的记录: s_date需要使用之前的
       case when NVL(curr.phone, '') = NVL(prev.phone, '') then prev.s_date
            else NVL(curr.s_date, prev.s_date)
            end as s_date,
       NVL(curr.e_date, prev.e_date) as e_date
from (
  select cust_id, phone, DATE(from_unixtime(unix_timestamp(dt, 'yyyyMMdd'), 'yyyy-MM-dd')) as s_date, DATE('9999-12-31') as e_date
  from datadev.zipper_table_test_cust_src
  where dt = '${etldate}'
) as curr

left join (
  select cust_id, phone, s_date, if(e_date > from_unixtime(unix_timestamp('${etldate}', 'yyyyMMdd'), 'yyyy-MM-dd'), DATE('9999-12-31'), e_date) as e_date,
         row_number() over(partition by cust_id order by e_date desc) as r_num -- 取最新状态
  from datadev.zipper_table_test_cust_dst
  where regexp_replace(s_date, '-', '') <= '${etldate}' -- 拉链表历史数据回滚
) as prev
on curr.cust_id = prev.cust_id
and prev.r_num = 1

union all

-- part2: 处理删除的记录,以及有变化的记录中的旧记录
select prev_cust.cust_id, prev_cust.phone, prev_cust.s_date,
       case when e_date <> '9999-12-31' then e_date
            else DATE(from_unixtime(unix_timestamp('${etldate}', 'yyyyMMdd'), 'yyyy-MM-dd'))
            END as e_date
from (
  select cust_id, phone, s_date, if(e_date > from_unixtime(unix_timestamp('${etldate}', 'yyyyMMdd'), 'yyyy-MM-dd'), DATE('9999-12-31'), e_date) as e_date
  from datadev.zipper_table_test_cust_dst
  where regexp_replace(s_date, '-', '') <= '${etldate}' -- 拉链表历史数据回滚
) as prev_cust

left join (
  select cust_id, phone
  from datadev.zipper_table_test_cust_src
  where dt = '${etldate}'
) as curr_cust
on curr_cust.cust_id = prev_cust.cust_id
-- 只要变化量
where NVL(prev_cust.phone, '') <> NVL(curr_cust.phone, '')
;

4. 테스트

4.1 첫날(20210601): ${etldate}를 20210601로 바꾸고 SQL을 실행한다. 초기상태이며 고객정보에 변동이 없으므로 시행일은 2021-06-01이며 시행일은 9999-12-31(현재 유효함을 의미)입니다.

zipper_table_test_cust_dst.cust_id zipper_table_test_cust_dst.전화 zipper_table_test_cust_dst.s_date zipper_table_test_cust_dst.e_date
001 1111 2021-06-01 9999-12-31
002 2222 2021-06-01 9999-12-31
003 3333 2021-06-01 9999-12-31
004 4444 2021-06-01 9999-12-31

4.2 둘째 날(20210602): ${etldate}를 20210602로 바꾸고 SQL을 실행한다. 이때 원래 테이블은 002와 004의 휴대폰 번호를 수정했기 때문에 두 개의 레코드가 있을 것입니다. 하나는 데이터의 과거 상태를 기록하고 다른 하나는 데이터의 현재 상태를 기록합니다. 그런 다음 원본 테이블에도 005 고객이 추가되었으므로 이때 데이터의 유효 날짜는 2021-06-02이고 만료 날짜는 9999-12-31입니다.

zipper_table_test_cust_dst.cust_id zipper_table_test_cust_dst.전화 zipper_table_test_cust_dst.s_date zipper_table_test_cust_dst.e_date
001 1111 2021-06-01 9999-12-31
002 2222 2021-06-01 2021-06-02
002 2222-1 2021-06-02 9999-12-31
003 3333 2021-06-01 9999-12-31
004 4444 2021-06-01 2021-06-02
004 4444-1 2021-06-02 9999-12-31
005 5555 2021-06-02 9999-12-31

4.3 셋째 날(20210603): ${etldate}를 20210602로 바꾸고 SQL을 실행합니다. 이때 원래 테이블은 001, 002, 005를 수정하고 006을 추가했습니다.

zipper_table_test_cust_dst.cust_id zipper_table_test_cust_dst.전화 zipper_table_test_cust_dst.s_date zipper_table_test_cust_dst.e_date
001 1111 2021-06-01 2021-06-03
001 1111-1 2021-06-03 9999-12-31
002 2222 2021-06-01 2021-06-02
002 2222-1 2021-06-02 2021-06-03
002 2222-2 2021-06-03 9999-12-31
003 3333 2021-06-01 9999-12-31
004 4444 2021-06-01 2021-06-02
004 4444-1 2021-06-02 9999-12-31
005 5555 2021-06-02 2021-06-03
005 5555-1 2021-06-03 9999-12-31
006 6666 2021-06-03 9999-12-31

4.4 넷째 날(20210604): ${etldate}를 20210602로 바꾸고 SQL을 실행합니다. 이때 원본 테이블은 002를 업데이트하고 007을 추가하고 001을 삭제했습니다. 삭제할 때 데이터 만료 날짜는 현재 날짜로 변경되어야 합니다.

zipper_table_test_cust_dst.cust_id zipper_table_test_cust_dst.전화 zipper_table_test_cust_dst.s_date zipper_table_test_cust_dst.e_date
001 1111 2021-06-01 2021-06-03
001 1111-1 2021-06-03 2021-06-04
002 2222 2021-06-01 2021-06-02
002 2222-1 2021-06-02 2021-06-03
002 2222-2 2021-06-03 2021-06-04
002 2222-3 2021-06-04 9999-12-31
003 3333 2021-06-01 9999-12-31
004 4444 2021-06-01 2021-06-02
004 4444-1 2021-06-02 9999-12-31
005 5555 2021-06-02 2021-06-03
005 5555-1 2021-06-03 9999-12-31
006 6666 2021-06-03 9999-12-31
007 7777 2021-06-04 9999-12-31

다섯, 지퍼 테이블의 데이터 롤백 새로 고침

지퍼 테이블의 최신 상태는 다음 코드를 통해 확인할 수 있습니다.

select * from datadev.zipper_table_test_cust_dst where e_date = '9999-12-31';

다음 코드를 통해 지퍼 테이블의 과거 상태/스냅샷 보기

-- 查看拉链表的20210602的快照
select cust_id, phone, s_date, if(e_date > '2021-06-02', DATE('9999-12-31'), e_date) as e_date
from datadev.zipper_table_test_cust_dst
where s_date <= '2021-06-02'; 

따라서 지퍼 테이블의 데이터 롤백 새로 고침을 위해서는 어필 코드에 따라 그날의 과거 스냅샷을 찾아서 새로 고침하면 됩니다. (참고: 위에 게시한 지퍼 테이블 삽입 문에는 이미 데이터 롤백 및 새로 고침 기능이 포함되어 있습니다. 독자가 직접 테스트할 수 있습니다. ${etldate}를 롤백할 날짜로 바꾼 다음 INSERT OVERWRITE TABLE 행을 주석 처리하고 select를 실행하여 결과를 확인하십시오.)

六、另一种实现

上一种实现方式有一个缺点,随着拉链表数据量的增多,每次执行的时间也会随之增多。因此,需要改进:可采用hive结合ES的方式。

-- 拉链表(hive只存储新增/更新量,全量存储于ES)实现代码

-- 临时表,只存放T-1天的新增以及变化的记录
CREATE TABLE IF NOT EXISTS datadev.zipper_table_test_cust_dst_2 (
  `id` STRING COMMENT 'es id',
  `cust_id` STRING COMMENT '客户编号',
  `phone` STRING COMMENT '手机号码',
  `s_date` DATE COMMENT '生效时间',
  `e_date` DATE COMMENT '失效时间'
)STORED AS ORC
TBLPROPERTIES ("orc.compress"="SNAPPY")
;

drop table datadev.zipper_table_test_cust_dst_2;

select * from datadev.zipper_table_test_cust_dst_2 a;




INSERT OVERWRITE TABLE datadev.zipper_table_test_cust_dst_2
  select concat_ws('-', curr.s_date, curr.cust_id) as id,
         curr.cust_id as cust_id,
         curr.phone as phone,
         DATE(curr.s_date) as s_date,
         DATE('9999-12-31') as e_date
  from (
    select cust_id, phone, from_unixtime(unix_timestamp(dt, 'yyyyMMdd'), 'yyyy-MM-dd') as s_date
    from datadev.zipper_table_test_cust_src
    where dt = '20210603' -- etldate 
  ) as curr

    left join (
      select *
      from datadev.zipper_table_test_cust_src
      where dt = '20210602' -- prev_date
    ) as prev
      on prev.cust_id = curr.cust_id
  where NVL(curr.phone, '') <> NVL(prev.phone, '')

  union all

  select concat_ws('-', STRING(prev.s_date), prev.cust_id) as id,
         prev.cust_id as cust_id,
         prev.phone as phone,
         prev.s_date as s_date,
         case when NVL(prev.phone, '') = NVL(curr.phone, '') then prev.e_date
         else DATE(from_unixtime(unix_timestamp(dt, 'yyyyMMdd'), 'yyyy-MM-dd'))
         end as e_date
  from (
    select cust_id, phone, s_date, e_date,
    -- 只更新最新的一条
    row_number() over(partition by cust_id order by s_date desc) as r_num
    from datadev.zipper_table_test_cust_dst_2
  ) as prev
  
  inner join (
      select *
      from datadev.zipper_table_test_cust_src
      where dt = '20210603' -- etldate 
  ) as curr
  on prev.cust_id = curr.cust_id
  where prev.r_num = 1 
  ;
  
  
   
  
-- mock: load delta data to es
CREATE TABLE IF NOT EXISTS datadev.es_zipper (
  `id` STRING COMMENT 'es id',
  `cust_id` STRING COMMENT '客户编号',
  `phone` STRING COMMENT '手机号码',
  `s_date` DATE COMMENT '生效时间',
  `e_date` DATE COMMENT '失效时间'
)STORED AS ORC
TBLPROPERTIES ("orc.compress"="SNAPPY")
;  

drop table datadev.es_zipper;

select * from datadev.es_zipper;


INSERT OVERWRITE TABLE datadev.es_zipper
SELECT nvl(curr.id, prev.id) as id,
nvl(curr.cust_id, prev.cust_id) as cust_id,
nvl(curr.phone, prev.phone) as phone,
nvl(curr.s_date, prev.s_date) as s_date,
nvl(curr.e_date, prev.e_date) as e_date
FROM datadev.es_zipper prev

full join datadev.zipper_table_test_cust_dst_2 curr
on curr.id = prev.id;

추천

출처blog.csdn.net/qq_37771475/article/details/118112246