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:
- 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.
- 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
- 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:
- Registro recién agregado: la fecha de vigencia de los datos es hoy, la fecha de vencimiento es 9999-12-31
- Registros sin cambios: la fecha de vigencia de los datos debe usarse antes y la fecha de vencimiento permanece sin cambios
- 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
- 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;