データウェアハウスにおけるジッパーテーブルの設計と実装

1. はじめに

  • 増分テーブル: 増分データ、つまり新しい増分と変更を保存するための日付パーティションがあります。
  • フルスケールテーブル:日付パーティションなし(毎日上書き更新)、現時点でのデータの最新状態を保存するため、データの履歴変更を記録できません
  • スナップショット テーブル: 日付パーティションがあり、データは毎日いっぱいになります (変更があるかどうかに関係なく)。欠点は、各パーティションに大量の重複データが保存され、ストレージ スペースが無駄になることです。
  • ジッパー テーブル: ジッパー テーブルは、履歴状態と最新の状態データを維持するために使用されるテーブルです。ジッパーの粒度の違いによると、ジッパー テーブルは実際にはスナップショットと同等ですが、一部の未変更のレコードを削除するように最適化されています。

2. 応用シナリオ

ジッパー テーブルは、大量のデータがありフィールド変更の割合と頻度が小さいときに履歴スナップショット情報を表示する必要があるシナリオに適しています。

たとえば、数千万のレコードと数百のフィールドを持つ顧客テーブルがあるとします。したがって、この種のテーブルの場合、ORC 圧縮を使用したとしても、1 つのテーブルのデータ保存容量は 1 日あたり 50GB を超え、HDFS で 3 つのバックアップを使用する場合、保存容量はさらに大きくなります。

では、このテーブルをどのように設計すればよいでしょうか? 以下にいくつかのオプションがあります。

  1. オプション 1 (フルスケール テーブル): 毎日最新のデータを抽出して前日のデータを上書きする利点は実装が簡単でスペースを節約できることですが、欠点も明らかであり、履歴状態がありません
  2. 解決策 2 (スナップショット テーブル): 毎日満杯にすると、履歴データを表示できますが、欠点は、ストレージ容量が大きすぎることです。特に顧客情報が頻繁に変更されない場合、フィールドの繰り返しストレージ率が高すぎることです。
  3. 解決策 3 (ジッパー テーブル): ジッパー テーブルの設計を使用すると、履歴ステータスを表示できるだけでなく、占有するストレージ スペースも非常に少なくて済みます (結局のところ、変更されていないデータを繰り返し保存する必要はありません)。

3. Hive 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を削除

さて、本題に戻りますが、ジッパーテーブルをどのようにデザインするか?

まず、ジッパー テーブルには、データ有効日データ有効期限という 2 つの重要な監査フィールドがあります。名前が示すように、データ有効日はレコードが有効になった日を記録し、データ有効期限はレコードの有効期限を記録します (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 1日目(20210601): ${etldate}を20210601に置き換えてSQLを実行します。これは初期状態であり、顧客情報に変更はないため、発効日は2021-06-01、発効日は9999-12-31(現在有効という意味)となります。

ziper_table_test_cust_dst.cust_id ziper_table_test_cust_dst.phone ziper_table_test_cust_dst.s_date ziper_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 2日目(20210602): ${etldate}を20210602に置き換えてSQLを実行します。この時点で、元のテーブルは携帯電話番号 002 と 004 を変更しているため、2 つのレコードが存在し、1 つはデータの過去の状態を記録し、もう 1 つはデータの現在の状態を記録します。元のテーブルには 005 人の顧客も追加されているため、この時点でのデータの有効日は 2021-06-02、有効期限は 9999-12-31 になります。

ziper_table_test_cust_dst.cust_id ziper_table_test_cust_dst.phone ziper_table_test_cust_dst.s_date ziper_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 3日目(20210603): ${etldate}を20210602に置き換えてSQLを実行します。この時点で、元のテーブルの 001、002、005 が変更され、006 が追加されています。

ziper_table_test_cust_dst.cust_id ziper_table_test_cust_dst.phone ziper_table_test_cust_dst.s_date ziper_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 4日目(20210604): ${etldate}を20210602に置き換えてSQLを実行します。このとき、元のテーブルは002が更新、007が追加、001が削除されています。なお、削除する際にはデータの有効期限を当日に変更する必要があります。

ziper_table_test_cust_dst.cust_id ziper_table_test_cust_dst.phone ziper_table_test_cust_dst.s_date ziper_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

5、ジッパーテーブルのデータロールバックリフレッシュ

ジッパー テーブルの最新のステータスは、次のコードで確認できます。

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