一、什么是拉链表
针对订单表、订单商品表,流水表,这些表中的数据是比较多的,如果使用全量的方式,会造成大量的数据冗余,浪费磁盘空间。
所以这种表,一般使用增量的方式,每日采集新增的数据。
在这注意一点:针对订单表,如果单纯的按照订单产生时间增量采集数据,是有问题的,因为用户可能今天下单,明天才支付,但是Hive是不支持数据更新的,这样虽然MySQL中订单的状态改变了,但是Hive中订单的状态还是之前的状态。
想要解决这个问题,一般有这么几种方案:
第一种:每天全量导入订单表的数据,这种方案在项目启动初期是没有多大问题的,因为前期数据量不大,但是随着项目的运营,订单量暴增,假设每天新增1亿订单,之前已经累积了100亿订单,如果每天都是全量导入的话,那也就意味着每天都需要把数据库中的100多亿订单数据导入到HDFS中保存一份,这样会极大的造成数据冗余,太浪费磁盘空间了。
第二种:只保存当天的全量订单表数据,每次在导入之前,删除前一天保存的全量订单数据,这种方式虽然不会造成数据冗余,但是无法查询订单的历史状态,只有当前的最新状态,也不太好。
第三种:拉链表,这种方式在普通增量导入方式的基础之上进行完善,把变化的数据也导入进来,这样既不会造成大量的数据冗余,还可以查询订单的历史状态。
拉链表是针对数据仓库设计中表存储数据的方式而定义的,顾名思义,所谓 拉链,就是记录历史。记录一个事物从开始,一直到当前状态的所有历史变化的信息。
下面就是一张拉链表,存储的是用户的最基本信息以及每条记录的生命周期。
我们可以使用这张表拿到当天的最新数据以及之前的历史数据。
用户编号 手机号码 start_time end_time 解释
001 1111 2026-01-01 9999-12-31 初始数据
002 2222 2026-01-01 2026-01-01 初始数据
003 3333 2026-01-01 9999-12-31 初始数据
002 2333 2026-01-02 9999-12-31 修改
004 4444 2026-01-03 9999-12-31 新增
说明:
start_time 表示该条记录的生命周期开始时间,end_time 表示该条记录的生命周期结束时间;
end_time ='9999-12-31’表示该条记录目前处于有效状态;
如果查询当前所有有效的记录,则使用 SQL
select * from user where end_time ='9999-12-31'
如果查询 2026-01-02 的历史快照【获取指定时间内的有效数据】,则使用SQL
select * from user where start_time <= '2026-01-02' and end_time >= '2026-01-02'
这就是拉链表。
二、如何制作拉链表
那针对我们前面分析的订单表,希望使用拉链表的方式实现数据采集,因为每天都保存全量订单数据比较浪费磁盘空间,但是只采集增量的话无法反应订单的状态变化。
所以需要既采集增量,还要采集订单状态变化了的数据。
针对订单表中的订单状态字段有这么几个阶段
未支付
已支付
未发货
已发货
在这我们先分析两种状态:未支付和已支付。
我们先举个例子:
假设我们的系统是2026年3月1日开始运营的
那么到3月1日结束订单表所有数据如下:
订单id 创建时间 更新时间 订单状态 解释
001 2026-03-01 null 未支付 新增
002 2026-03-01 2026-03-01 已支付 新增
3月2日结束订单表所有数据如下:
订单id 创建时间 更新时间 订单状态 解释
001 2026-03-01 2026-03-02 已支付 修改
002 2026-03-01 2026-03-01 已支付
003 2026-03-02 2026-03-02 已支付 新增
基于订单表中的这些数据如何制作拉链表?
实现思路
1:首先针对3月1号中的订单数据构建初始的拉链表,拉链表中需要有一个start_time(数据生效开始时间)和end_time(数据生效结束时间),默认情况下start_time等于表中的创建时间,end_time初始化为一个无限大的日期9999-12-31
将3月1号的订单数据导入到拉链表中。
此时拉链表中数据如下:
订单id 订单状态 start_time end_time
001 未支付 2026-03-01 9999-12-31
002 已支付 2026-03-01 9999-12-31
2:在3月2号的时候,需要将订单表中发生了变化的数据和新增的订单数据整合到之前的拉链表中
此时需要先创建一个每日更新表,将每日新增和变化了的数据保存到里面
然后基于拉链表和这个每日更新表进行left join,根据订单id进行关联,如果可以关联上,就说明这个订单的状态发生了变化,然后将订单状态发生了变化的数据的end_time改为2026-03-01(当天日期-1天)
然后再和每日更新表中的数据执行union all操作,将结果重新insert到拉链表中
最终拉链表中的数据如下:
订单id 订单状态 start_time end_time
001 未支付 2026-03-01 2026-03-01
002 已支付 2026-03-01 9999-12-31
001 已支付 2026-03-02 9999-12-31
003 已支付 2026-03-02 9999-12-31
解释:
因为在3月2号的时候,订单id为001的数据的订单状态发生了变化,所以拉链表中订单id为001的原始数据的end_time需要修改为2026-03-01。
然后需要新增一条订单id为001的数据,订单状态为已支付,start_time为2026-03-02,end_time为9999-12-31。
还需要将3月2号新增的订单id为003的数据也添加进来。
三、基于订单表的拉链表实现
下面我们开始实现:
1:首先初始化2026-03-01、2026-03-02和2026-03-03的订单表新增和变化的数据,ods_user_order(直接将数据初始化到HDFS中),这个表其实就是前面我们所说的每日更新表
注意:这里模拟使用sqoop从mysql中抽取新增和变化的数据,根据order_date和update_time这两个字段获取这些数据,所以此时ods_user_order中的数据就是每日的新增和变化了的数据。
1、生成数据
执行代码生成数据:
代码如下:
注意,修改校验码和hdfs地址。
package com.imooc.useraction;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.imooc.useraction.utils.GenerateDateAndActArrUtil;
import com.imooc.useraction.utils.HdfsOpUtil;
import com.imooc.useraction.utils.HttpUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 需求:生成订单表拉链基础数据
* 生成2026-03-01~2026-03-03的数据
*
*/
public class GenerateZipData {
private final static Logger logger = LoggerFactory.getLogger(GenerateZipData.class);
public static void main(String[] args) throws Exception{
//通过接口获取用户行为数据
String dataUrl = "http://data.xuwei.tech/d1/go3";
JSONObject paramObj = new JSONObject();
//TODO code:校验码,需要到微信公众号上获取有效校验码,具体操作流程见电子书
paramObj.put("code","JD3B37868104C5F2A");//校验码
JSONObject dataObj = HttpUtil.doPost(dataUrl, paramObj);
//logger.info(dataObj.toJSONString());
//判断获取的用户行为数据是否正确
boolean flag = dataObj.containsKey("error");
if(!flag){
long start = System.currentTimeMillis();
logger.info("===============start 上传数据==============");
//从dataObj中获取每一天,每一种类型的数据
String[] dateArr = GenerateDateAndActArrUtil.getZipDateArr();
for(int i=0;i<dateArr.length;i++){
String dt = dateArr[i];
String tableName = "user_order";
//获取某一天某一类型的数据
JSONArray resArr = dataObj.getJSONArray(tableName+"_"+dt);
StringBuffer sb = new StringBuffer();
for(int m=0;m<resArr.size();m++){
JSONObject jsonObj = resArr.getJSONObject(m);
String line = jsonObj.getString("line");
if(m==0){
sb.append(line);
}else{
sb.append("\n"+line);
}
}
//将数据上传到HDFS上面,注意:需要关闭HDFS的权限校验机制
String hdfsOutPath = "hdfs://bigdata01:9000/data/ods/"+tableName+"/"+dt.replace("-","");
String fileName = tableName+"-"+dt+".log";
logger.info("开始上传:"+hdfsOutPath+"/"+fileName);
HdfsOpUtil.put(sb.toString(),hdfsOutPath,fileName);
}
logger.info("===============end 上传数据==============");
long end = System.currentTimeMillis();
logger.info("===============耗时: "+(end-start)/1000+"秒===============");
}else{
logger.error("上传用户行为数据失败:"+dataObj.toJSONString());
}
}
}
2、加载数据
ods_user_order在前面已经使用过,所以在这只需要将2026-03-01、2026-03-02和2026-03-03的数据加载进去即可
hive (default)> alter table ods_mall.ods_user_order add if not exists partition(dt='20260301') location '20260301';
hive (default)> alter table ods_mall.ods_user_order add if not exists partition(dt='20260302') location '20260302';
hive (default)> alter table ods_mall.ods_user_order add if not exists partition(dt='20260303') location '20260303';
3、dwd_user_order生成数据
hive (default)> insert overwrite table dwd_mall.dwd_user_order partition(dt='20260301') select
order_id,
order_date,
user_id,
order_money,
order_type,
order_status,
pay_id,
update_time
from ods_mall.ods_user_order
where dt = '20260301' and order_id is not null;
hive (default)> insert overwrite table dwd_mall.dwd_user_order partition(dt='20260302') select
order_id,
order_date,
user_id,
order_money,
order_type,
order_status,
pay_id,
update_time
from ods_mall.ods_user_order
where dt = '20260302' and order_id is not null;
hive (default)> insert overwrite table dwd_mall.dwd_user_order partition(dt='20260303') select
order_id,
order_date,
user_id,
order_money,
order_type,
order_status,
pay_id,
update_time
from ods_mall.ods_user_order
where dt = '20260303' and order_id is not null;
4、创建拉链表
基于每日更新订单表构建拉链表中的数据,创建拉链表:dws_user_order_zip
create external table if not exists dws_mall.dws_user_order_zip(
order_id bigint,
order_date string,
user_id bigint,
order_money double,
order_type int,
order_status int,
pay_id bigint,
update_time string,
start_time string,
end_time string
)row format delimited
fields terminated by '\t'
location 'hdfs://bigdata01:9000/data/dws/user_order_zip/';
5、向拉链表中添加数据
(1)添加2026-03-01的全量数据至拉链表(初始化操作)
insert overwrite table dws_mall.dws_user_order_zip
select *
from
(
select
duoz.order_id,
duoz.order_date,
duoz.user_id,
duoz.order_money,
duoz.order_type,
duoz.order_status,
duoz.pay_id,
duoz.update_time,
duoz.start_time,
case
when duoz.end_time = '9999-12-31' and duo.order_id is not null then date_add('2026-03-01',-1)
else duoz.end_time
end as end_time
from dws_mall.dws_user_order_zip as duoz
left join
(
select
order_id
from dwd_mall.dwd_user_order
where dt='20260301'
)as duo
on duoz.order_id = duo.order_id
union all
select
duo.order_id,
duo.order_date,
duo.user_id,
duo.order_money,
duo.order_type,
duo.order_status,
duo.pay_id,
duo.update_time,
'2026-03-01' as start_time,
'9999-12-31' as end_time
from dwd_mall.dwd_user_order as duo
where duo.dt='20260301'
) as t;
查询此时拉链表中的数据。
hive (default)> select * from dws_mall.dws_user_order_zip;
OK
30001 2026-03-01 10:10:10 10096 200.0 1 0 NULL NULL 2026-03-01 9999-12-31
30002 2026-03-01 10:10:10 10096 200.0 1 0 NULL NULL 2026-03-01 9999-12-31
30003 2026-03-01 10:10:10 10096 200.0 1 1 400012026-03-01 11:11:11 2026-03-01 9999-12-31
Time taken: 0.176 seconds, Fetched: 3 row(s)
(2)添加2026-03-02的新增及变化的数据至拉链表
insert overwrite table dws_mall.dws_user_order_zip
select *
from
(
select
duoz.order_id,
duoz.order_date,
duoz.user_id,
duoz.order_money,
duoz.order_type,
duoz.order_status,
duoz.pay_id,
duoz.update_time,
duoz.start_time,
case
when duoz.end_time = '9999-12-31' and duo.order_id is not null then date_add('2026-03-02',-1)
else duoz.end_time
end as end_time
from dws_mall.dws_user_order_zip as duoz
left join
(
select
order_id
from dwd_mall.dwd_user_order
where dt='20260302'
)as duo
on duoz.order_id = duo.order_id
union all
select
duo.order_id,
duo.order_date,
duo.user_id,
duo.order_money,
duo.order_type,
duo.order_status,
duo.pay_id,
duo.update_time,
'2026-03-02' as start_time,
'9999-12-31' as end_time
from dwd_mall.dwd_user_order as duo
where duo.dt='20260302'
) as t;
查询此时拉链表中的数据
hive (default)>select * from dws_mall.dws_user_order_zip;
OK
30001 2026-03-01 10:10:10 10096 200.0 1 0 NULL NULL 2026-03-01 9999-12-31
30002 2026-03-01 10:10:10 10096 200.0 1 0 NULL NULL 2026-03-01 2026-03-01
30003 2026-03-01 10:10:10 10096 200.0 1 1 400012026-03-01 11:11:11 2026-03-01 9999-12-31
30002 2026-03-01 10:10:10 10096 200.0 1 1 400022026-03-02 11:11:11 2026-03-02 9999-12-31
30004 2026-03-02 10:10:10 10096 200.0 1 0 NULL NULL 2026-03-02 9999-12-31
30005 2026-03-02 10:10:10 10096 200.0 1 1 400032026-03-02 11:11:11 2026-03-02 9999-12-31
Time taken: 0.186 seconds, Fetched: 6 row(s)
(3)查询目前的所有有效数据
hive (default)> select * from dws_mall.dws_user_order_zip where end_time='9999-12-31';
OK
30001 2026-03-01 10:10:10 10096 200.0 1 0 NULL NULL 2026-03-01 9999-12-31
30003 2026-03-01 10:10:10 10096 200.0 1 1 400012026-03-01 11:11:11 2026-03-01 9999-12-31
30002 2026-03-01 10:10:10 10096 200.0 1 1 400022026-03-02 11:11:11 2026-03-02 9999-12-31
30004 2026-03-02 10:10:10 10096 200.0 1 0 NULL NULL 2026-03-02 9999-12-31
30005 2026-03-02 10:10:10 10096 200.0 1 1 400032026-03-02 11:11:11 2026-03-02 9999-12-31
Time taken: 0.269 seconds, Fetched: 5 row(s)
(4)查询3月1日的切片数据
hive (default)> select * from dws_mall.dws_user_order_zip where start_time<='2026-03-01' and end_time>='2026-03-01';
OK
30001 2026-03-01 10:10:10 10096 200.0 1 0 NULL NULL 2026-03-01 9999-12-31
30002 2026-03-01 10:10:10 10096 200.0 1 0 NULL NULL 2026-03-01 2026-03-01
30003 2026-03-01 10:10:10 10096 200.0 1 1 400012026-03-01 11:11:11 2026-03-01 9999-12-31
Time taken: 0.182 seconds, Fetched: 3 row(s)
(5) 查询3月2日的切片数据
hive (default)> select * from dws_mall.dws_user_order_zip where start_time<='2026-03-02' and end_time>='2026-03-02';
OK
30001 2026-03-01 10:10:10 10096 200.0 1 0 NULL NULL 2026-03-01 9999-12-31
30003 2026-03-01 10:10:10 10096 200.0 1 1 400012026-03-01 11:11:11 2026-03-01 9999-12-31
30002 2026-03-01 10:10:10 10096 200.0 1 1 400022026-03-02 11:11:11 2026-03-02 9999-12-31
30004 2026-03-02 10:10:10 10096 200.0 1 0 NULL NULL 2026-03-02 9999-12-31
30005 2026-03-02 10:10:10 10096 200.0 1 1 400032026-03-02 11:11:11 2026-03-02 9999-12-31
Time taken: 0.216 seconds, Fetched: 5 row(s)
(6)添加2026-03-03的新增及变化的数据至拉链表
insert overwrite table dws_mall.dws_user_order_zip
select *
from
(
select
duoz.order_id,
duoz.order_date,
duoz.user_id,
duoz.order_money,
duoz.order_type,
duoz.order_status,
duoz.pay_id,
duoz.update_time,
duoz.start_time,
case
when duoz.end_time = '9999-12-31' and duo.order_id is not null then date_add('2026-03-03',-1)
else duoz.end_time
end as end_time
from dws_mall.dws_user_order_zip as duoz
left join
(
select
order_id
from dwd_mall.dwd_user_order
where dt='20260303'
)as duo
on duoz.order_id = duo.order_id
union all
select
duo.order_id,
duo.order_date,
duo.user_id,
duo.order_money,
duo.order_type,
duo.order_status,
duo.pay_id,
duo.update_time,
'2026-03-03' as start_time,
'9999-12-31' as end_time
from dwd_mall.dwd_user_order as duo
where duo.dt='20260303'
) as t;
(7)查询目前的所有有效数据
hive (default)> select * from dws_mall.dws_user_order_zip where end_time='9999-12-31';
OK
30003 2026-03-01 10:10:10 10096 200.0 1 1 400012026-03-01 11:11:11 2026-03-01 9999-12-31
30002 2026-03-01 10:10:10 10096 200.0 1 1 400022026-03-02 11:11:11 2026-03-02 9999-12-31
30005 2026-03-02 10:10:10 10096 200.0 1 1 400032026-03-02 11:11:11 2026-03-02 9999-12-31
30001 2026-03-01 10:10:10 10096 200.0 1 1 400042026-03-03 11:11:11 2026-03-03 9999-12-31
30004 2026-03-02 10:10:10 10096 200.0 1 1 400052026-03-03 11:11:11 2026-03-03 9999-12-31
30006 2026-03-03 10:10:10 10096 200.0 1 1 400062026-03-03 11:11:11 2026-03-03 9999-12-31
30007 2026-03-03 10:10:10 10096 200.0 1 0 NULL NULL 2026-03-03 9999-12-31
Time taken: 0.359 seconds, Fetched: 7 row(s)
(8)查询3月1日的切片数据
hive (default)> select * from dws_mall.dws_user_order_zip where start_time<='2026-03-01' and end_time>='2026-03-01';
OK
30001 2026-03-01 10:10:10 10096 200.0 1 0 NULL NULL 2026-03-01 2026-03-02
30002 2026-03-01 10:10:10 10096 200.0 1 0 NULL NULL 2026-03-01 2026-03-01
30003 2026-03-01 10:10:10 10096 200.0 1 1 400012026-03-01 11:11:11 2026-03-01 9999-12-31
Time taken: 0.328 seconds, Fetched: 3 row(s)
(9)查询3月2日的切片数据
hive (default)> select * from dws_mall.dws_user_order_zip where start_time<='2026-03-02' and end_time>='2026-03-02';
OK
30001 2026-03-01 10:10:10 10096 200.0 1 0 NULL NULL 2026-03-01 2026-03-02
30003 2026-03-01 10:10:10 10096 200.0 1 1 400012026-03-01 11:11:11 2026-03-01 9999-12-31
30002 2026-03-01 10:10:10 10096 200.0 1 1 400022026-03-02 11:11:11 2026-03-02 9999-12-31
30004 2026-03-02 10:10:10 10096 200.0 1 0 NULL NULL 2026-03-02 2026-03-02
30005 2026-03-02 10:10:10 10096 200.0 1 1 400032026-03-02 11:11:11 2026-03-02 9999-12-31
Time taken: 0.951 seconds, Fetched: 5 row(s)
(10)查询3月3日的切片数据
hive (default)> select * from dws_mall.dws_user_order_zip where start_time<='2026-03-03' and end_time>='2026-03-03';
OK
30003 2026-03-01 10:10:10 10096 200.0 1 1 400012026-03-01 11:11:11 2026-03-01 9999-12-31
30002 2026-03-01 10:10:10 10096 200.0 1 1 400022026-03-02 11:11:11 2026-03-02 9999-12-31
30005 2026-03-02 10:10:10 10096 200.0 1 1 400032026-03-02 11:11:11 2026-03-02 9999-12-31
30001 2026-03-01 10:10:10 10096 200.0 1 1 400042026-03-03 11:11:11 2026-03-03 9999-12-31
30004 2026-03-02 10:10:10 10096 200.0 1 1 400052026-03-03 11:11:11 2026-03-03 9999-12-31
30006 2026-03-03 10:10:10 10096 200.0 1 1 400062026-03-03 11:11:11 2026-03-03 9999-12-31
30007 2026-03-03 10:10:10 10096 200.0 1 0 NULL NULL 2026-03-03 9999-12-31
Time taken: 0.195 seconds, Fetched: 7 row(s)
(11)查看某一个订单的历史变化情况
此时,如果想要翻旧账,查看某一个订单的历史变化情况,可以这样查询
hive (default)> select * from dws_mall.dws_user_order_zip where order_id = '30001';
OK
30001 2026-03-01 10:10:10 10096 200.0 1 0 NULL NULL 2026-03-01 2026-03-02
30001 2026-03-01 10:10:10 10096 200.0 1 1 400042026-03-03 11:11:11 2026-03-03 9999-12-31
四、拉链表的性能问题
拉链表也会遇到查询性能的问题,假设我们存放了5年的拉链数据,那么这张表势必会比较大,当查询的时候性能就比较低了
可以用以下思路来解决:
1、可以尝试对start_time和end_time做索引,这样可以提高一些性能。
2、保留部分历史数据,我们可以在一张表里面存放全量的拉链表数据,然后再对外暴露一张只提供近3个月数据的拉链表