Diseño e implementación de mesa zipper en data warehouse

1. Introducción

  • Tabla incremental: Existen particiones de fecha para almacenar datos incrementales, es decir, nuevos incrementos y cambios.
  • Tabla de escala completa: sin partición de fecha (sobrescribir y actualizar todos los días), almacenar el último estado de datos a partir de ahora, por lo que los cambios históricos de datos no se pueden registrar
  • Tabla de instantáneas: hay particiones de fecha y los datos están llenos todos los días (haya un cambio o no). La desventaja es que cada partición almacena una gran cantidad de datos duplicados, lo que desperdicia espacio de almacenamiento.
  • Tabla de cremalleras: la tabla de cremalleras es una tabla que se utiliza para mantener el estado histórico y los datos de estado más recientes. De acuerdo con la granularidad diferente de las cremalleras, la tabla de cremalleras es en realidad equivalente a una instantánea, pero se ha optimizado para eliminar algunos registros sin cambios.

2. Escenarios de aplicación

La tabla zip es adecuada para escenarios en los que hay una gran cantidad de datos y es necesario ver la información histórica instantánea cuando la proporción y la frecuencia de los cambios de campo son pequeñas .

Por ejemplo, hay una tabla de clientes con decenas de millones de registros y cientos de campos. Entonces, para este tipo de tabla, incluso si se usa la compresión ORC, el espacio de almacenamiento de datos de una sola tabla superará los 50 GB por día. En el caso de usar tres copias de seguridad en HDFS, el espacio de almacenamiento será aún mayor.

Entonces, ¿cómo debo diseñar esta mesa? Aquí hay varias opciones:

  1. Opción 1 (tabla de escala completa): Extraiga los datos más recientes todos los días para sobrescribir los datos del día anterior. La ventaja es que es simple de implementar y ahorra espacio, pero la desventaja también es obvia, y no hay un estado histórico.
  2. Solución 2 (tabla de instantáneas): si se llena todos los días, podemos ver los datos históricos, pero la desventaja es que el espacio de almacenamiento es demasiado grande, especialmente cuando la información del cliente no cambia con frecuencia, la tasa de almacenamiento repetido del campo es demasiado alta
  3. Solución 3 (tabla con cremallera): si se adopta el diseño de la tabla con cremallera, no solo se puede ver el estado histórico, sino que también el uso del espacio de almacenamiento es extremadamente bajo (después de todo, los datos que no han cambiado no se almacenarán repetidamente)

3. Práctica de Hive SQL

Primero cree una tabla original de información del cliente para probar

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")
;

Luego inserte algunos datos de prueba

id_cliente teléfono 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

Una breve descripción de los datos es la siguiente:

  • 20210601 es la fecha de inicio, hay 4 clientes en total
  • 20210602 Se actualizó la información de los clientes 002 y 004, y se agregaron los clientes 005
  • 20210603 actualizó la información de 001, 002, 005 clientes y agregó 006 clientes
  • 20210604 Se actualizó la información del cliente 002, se agregó el cliente 007 y se eliminó el cliente 001

Ahora volviendo al tema, ¿cómo diseñar la mesa con cremallera?

En primer lugar, la tabla zip tiene dos campos de auditoría importantes: fecha de vigencia de los datos y fecha de vencimiento de los datos . Como su nombre lo indica, la fecha de vigencia de los datos registra cuándo entró en vigencia el registro y la fecha de vencimiento de los datos registra el tiempo de vencimiento del registro (9999-12-31 significa que ha sido válido hasta ahora). Entonces, las operaciones sobre los datos se pueden dividir en las siguientes categorías:

  1. Registro recién agregado: la fecha de vigencia de los datos es hoy, la fecha de vencimiento es 9999-12-31
  2. Registros sin cambios: la fecha de vigencia de los datos debe usarse antes y la fecha de vencimiento permanece sin cambios
  3. Registros con cambios: == "Para registros antiguos: mantenga y cambie la fecha de vencimiento a hoy; == "Para registros nuevos: agregue, la fecha de vigencia es hoy y la fecha de vencimiento es 9999-12-31
  4. Registros eliminados: es necesario cerrar el ciclo, la fecha de vencimiento se convierte en el mismo día

Por lo tanto, el código de implementación HQL de la tabla zip es el siguiente:

-- 拉链表建表语句
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. prueba

4.1 El primer día (20210601): Reemplace ${etldate} con 20210601 y ejecute SQL. Este es el estado inicial y no hay cambios en la información del cliente, por lo que la fecha de vigencia es 2021-06-01 y la fecha de vigencia es 9999-12-31 (lo que significa que actualmente es válida)

zipper_table_test_cust_dst.cust_id zipper_table_test_cust_dst.teléfono 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 El segundo día (20210602): Reemplace ${etldate} con 20210602 y ejecute SQL. En este momento, la tabla original ha modificado los números de teléfono móvil 002 y 004, por lo que habrá dos registros, uno registra el estado histórico de los datos y el otro registra el estado actual de los datos. Luego, la tabla original también agregó 005 clientes, por lo que la fecha de vigencia de los datos en este momento es 2021-06-02 y la fecha de vencimiento es 9999-12-31

zipper_table_test_cust_dst.cust_id zipper_table_test_cust_dst.teléfono 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 El tercer día (20210603): Reemplace ${etldate} con 20210602 y ejecute SQL. En este momento, la tabla original ha modificado 001, 002, 005 y ha agregado 006.

zipper_table_test_cust_dst.cust_id zipper_table_test_cust_dst.teléfono 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 El cuarto día (20210604): Reemplace ${etldate} con 20210602 y ejecute SQL. En este momento, la tabla original actualizó 002, agregó 007 y eliminó 001. Cabe señalar que al eliminar, la fecha de vencimiento de los datos debe cambiarse al día actual.

zipper_table_test_cust_dst.cust_id zipper_table_test_cust_dst.teléfono 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

Cinco, la actualización de reversión de datos de la tabla zip

El último estado de la tabla de cremalleras se puede ver a través del siguiente código

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

Vea el estado histórico/instantánea de la tabla de cremalleras a través del siguiente código

-- 查看拉链表的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'; 

Por lo tanto, para la actualización de reversión de datos de la tabla zip, solo necesitamos encontrar la instantánea histórica de ese día de acuerdo con el código de apelación y luego actualizarla. (Nota: la declaración de inserción de la tabla zip que publiqué anteriormente ya incluye la función de reversión y actualización de datos. Los lectores pueden probarlo por sí mismos: reemplace ${etldate} con la fecha que se revertirá y luego comente la línea INSERT OVERWRITE TABLE, simplemente ejecute select para ver los resultados)

六、另一种实现

上一种实现方式有一个缺点,随着拉链表数据量的增多,每次执行的时间也会随之增多。因此,需要改进:可采用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;

Supongo que te gusta

Origin blog.csdn.net/qq_37771475/article/details/118112246
Recomendado
Clasificación