Hive数据仓库实践——日期维度数据装载

本文涉及技术:Linux、Java、Hive、MySQL、Shell、Kettle、JavaScript、HDFS、存储过程。

需求:统计各商品的日销售额,月销售额,季度销售额以及年销售额;

在操作型数据库中直接使用SQL进行统计查询是可以完成这个需求的,但是会存在以下不足:

(1)统计查询会给原有业务增加压力;

(2)往往统计查询涉及的表比较多,SQL一般比较复杂;

(3)可能相关的数据不在同一个数据库,甚至不在同一个数据中心;

所以建设统一的数据仓库会是一个不错的选择,当然,这需要根据实际情况来决定是否有建设数据仓库的必要,因为数据仓库建设,历时较长,如果模型选择不好,后期维护相当复杂。这里以事实-维度模型(也称为星型模型)为例说明。它以某个事实为中心,利用多个维度对该事实进行统计。事实和维度在数据仓库中以表形式存在。比如上面的需求,销售额是事实表中的一列,日/月/季/年是时间维度表中粒度不同的列。下面首先来设计日期维度表

日期维度表设计

根据需求,日期维度表应当要包括日期(日),月,季,年以及表示标识行的键这些字段。表的键在数据仓库中有特别的名字,称为代理键,

代理键:关系型数据库中的主键可以看成是一种代理键,它和表中其他字段没有什么必要的关系,一般作为事实表与维度表关联,不过代理键并不要求唯一,但是推荐将代理键设计成唯一的;对于日期维度表,代理键有很多的设计方法,比如常见的

(1)uuid

(2)整数,类型需要根据数据量设计,如果数据量特别大可以选择bigint,但是一般int已经足够,因为int类型总共能表示40多亿数据

我们这里采用int类型,设计成年,月,日组成的整数,比如日期2010-02-24,可以使用整数20100224表示,这样设计的一个好处的是,每个日期的代理键都是唯一的,而且永远不会改变。

Hive和MySQL非常接近,可以按照MySQL的数据类型来设计Hive中的表,具体如下:

date_sk int, #代理键
time date,    #具体日期,格式:2010-02-23
month TINYINT,    #月 1~12
month_name varchar(9),    #月的名字 April
quarter TINYINT,    #季度 1~4
year SMALLINT    #年 4为数字组成,例如2010

 在设计表字段时,应当要充分估计字段的取值域,比如month,取值域1~12,选择tinyint类型即可(范围-128~127,占用1个字节)。比如quarter,在MySQL中可以设计成枚举类型,但是Hive中没有这种类型,所以选择tinyint类型。下面可以在Hive中创建日期维度表了,SQL如下:

create table date_dim(
date_sk int COMMENT 'surrogate key, which is independent of the other columns',
time date comment 'date,format:yyyy-mm-dd, for example,2018-01-23',
month TINYINT comment 'month,for example,January corresponds to the number 1',
month_name varchar(9) comment 'for example,January,February and so on',
quarter TINYINT COMMENT 'year is divided into four quarters,1 to 4',
year SMALLINT comment 'for example,2018'
)
comment 'a table for date dimension'
row format delimited fields terminated by ','
stored as textfile;

创建表时,每个字段应当给出注释或者使用在线文档进行维护,个人比较喜欢for example,因为有些字段说了未必能解释清楚,举个简单的例子,就直观的多。如果可以,全部采用英文。在Hive中执行SQL,如图:

可以登录HDFS页面查看,创建成功后,会生成一个date_dim目录,如图:

日期数据生成

Hive中不能直接生成代理键,我们在外部生成好日期数据后,再装载到Hive中,那么到底要生成多少日期数据呢?这个需要根据企业业务数据时间而定,可以选择企业所有业务数据中最早的时间作为日期维度表的第一个时间,而结束日期可以往后推算几年(以后需要再装载),几十年,几百年都可以(如果你的企业可以存活那么久的话)。

下面提供两种方式:

第一种方式在MySQL中使用存储过程生成日期数据,存储过程如下:

delimiter //
CREATE TABLE IF NOT EXISTS date_dim(
    date_sk int COMMENT 'surrogate key, which is independent of the other columns',
    date date comment 'date,format:yyyy-mm-dd, for example,2018-01-23',
    month TINYINT comment 'month,for example,January corresponds to the number 1',
    month_name varchar(9) comment 'for example,January,February and so on',
    quarter TINYINT COMMENT 'year is divided into four quarters,1 to 4',
    year SMALLINT comment 'for example,2018'
);
TRUNCATE TABLE date_dim;
DROP PROCEDURE IF EXISTS pro_date_dim;
CREATE PROCEDURE pro_date_dim (in start_date date,in end_date date)
BEGIN
	WHILE start_date<=end_date DO
			INSERT INTO date_dim(date_sk,date,month,month_name,quarter,year)VALUES(YEAR(start_date)*10000+MONTH(start_date)*100+DAY(start_date),start_date,MONTH(start_date),MONTHNAME(start_date),QUARTER(start_date),YEAR(start_date));
			SET start_date=ADDDATE(start_date,1);
	END WHILE;
	COMMIT;
END//
delimiter;

存储过程基本逻辑:如果表date_dim不存在,创建该表,否则先清空表中数据,然后判断存储过程pro_date_dim是否存在,如果存在,将该存储过程删除,否则,创建pro_date_dim存储过程。

在MySQL中执行上面的存储过程语句,然后执行如下语句来调用存储过程,生成日期数据:

CALL pro_date_dim('2000-01-01','2050-12-31')

生成的日期数据时间跨度从2000到2050年。这可能需要等待几分钟。虽然使用的存储过程,但并不能提高数据生成速度,只是方便了数据生成的逻辑。对于MySQL,可以采用批次插入的方式。例如:

INSERT INTO example
(example_id, name, value, other_value)
VALUES
(100, 'Name 1', 'Value 1', 'Other 1'),
(101, 'Name 2', 'Value 2', 'Other 2'),
(102, 'Name 3', 'Value 3', 'Other 3'),
(103, 'Name 4', 'Value 4', 'Other 4');

上面一次插入了4条数据,对于本例,可以在存储过程中拼接插入语句,一次插入一万条数据,整个生产过程只需要几秒中。速度提升还是很明显的。这里不再累述。数据生成好后,如图:

如果对Shell脚本很熟,也可以采用Shell脚本生成,Shell脚本如下:

# !/bin/bash
start_date=$1
end_date=$2

temp_date_full=`date -d $start_date +%F`
temp_start_second=`date -d $start_date +%s`
temp_end_second=`date -d $end_date +%s`

min=1
max=$[($temp_end_second-$temp_start_second)/(24*60*60)+1]

cat /dev/null > ./date_dim.csv

while [ $min -le $max ]
        do
                month=`date -d $temp_date_full +%m`
                month_name=`date -d $temp_date_full +%B`
                quarter=$[(10#$month-1)/3+1]
                year=`date -d $temp_date_full +%Y`
                day=`date -d $temp_date_full +%d`
                sk=$[10#$year*10000+10#$month*100+10#$day]
                echo ${sk}","${temp_date_full}","${month}","${month_name}","${quarter}","${year} >> ./date_dim.csv
                temp_date_full=`date -d "$temp_date_full 1 days" +%F`
                min=$[$min+1]
done

在当前Linux目录执行

./date_dim_create.sh 2000-01-01 2050-12-31

就可以在当前目录下生成日期数据文件date_dim.csv(需要为该脚本赋予执行权限)。如果对Shell脚本不熟,这里我们给出一些说明,帮助你理解

(1)# !/bin/bash

shell脚本中的注释使用#开头,但是这行有些与众不同,它是一种标识,标识是bash shell,类似于,一个html网页总是以<!DOCTYPE html>开头;

(2)$1

它表示从命令行提取第一个参数,也就是2000-01-01

(3)$start_date

shell脚本中有很多类似$start_date,它表示从start_date变量中取值,也就是说,如果变量start_date值是2000-01-01,那么$start_date就可以拿到这个值

(4)`date -d $start_date +%F`

shell有很多类似这样的脚本,首先这些字符串是使用反单引号包含的,它表示如果里面是命令,执行命令,+%后面字符是日期输出格式,例如F表示按照2000-01-01格式输出,s表示按秒输出,m表示按月输出等等;

(5)$[]

表达式运算,计算方括号里面的表达式值;

(6)cat /dev/null > ./date_dim.csv

表示清空文件内容,>是Linux的标准输出(覆盖方式输出);追加方式使用>>

如果对Shell不熟,编写出一个可以运行的脚本是非常困难的。

日期数据装载

第一种方式

因为Hive是基于HDFS的,在Linux上执行

hdfs dfs -put date_dim.csv /hive/warehouse/test.db/date_dim

直接将文件上传到HDFS的date_dim目录下,如图:

下面就可以进行查询了,如下:

如果查询不到数据,需要检查下date_dim.csv的权限。可以如下修改权限:

hdfs dfs -chmod 777 /hive/warehouse/test.db/date_dim

也可以使用hive直接加载数据,在Hive中,如下:

load data local inpath '/root/input/date_dim.csv' into table date_dim;

Kettle一键装载

作业

转换

执行流程,这个作业从start组件开始,首先在mysql中创建表,如果表已经存在删除表中数据,执行完毕进入设置变量组件,设置变量完成开始日期和结束日期,接下来的三个组件构成一个循环,首先验证开始日期是否小于小于结束日期,如果小于向mysql中插入数据,然后循环验证,直到退出(在验证中对开始日期加一天),数据插入完成,进入“Hive中创建表”组件,表创建成功后,从mysql中装载数据到hive,这个转换就是第二个图,在装载过程中,注意字段对应关系。关键代码如下:

设置变量

设置了两个变量,注意时间格式为2000/01/01,不然后面报错。日期验证

Date.prototype.format = function(fmt) { 
     var o = { 
        "M+" : this.getMonth()+1,                 //月份 
        "d+" : this.getDate(),                    //日 
        "h+" : this.getHours(),                   //小时 
        "m+" : this.getMinutes(),                 //分 
        "s+" : this.getSeconds(),                 //秒 
        "q+" : Math.floor((this.getMonth()+3)/3), //季度 
        "S"  : this.getMilliseconds()             //毫秒 
    }; 
    if(/(y+)/.test(fmt)) {
            fmt=fmt.replace(RegExp.$1, (this.getFullYear()+"").substr(4 - RegExp.$1.length)); 
    }
     for(var k in o) {
        if(new RegExp("("+ k +")").test(fmt)){
             fmt = fmt.replace(RegExp.$1, (RegExp.$1.length==1) ? (o[k]) : (("00"+ o[k]).substr((""+ o[k]).length)));
         }
     }
    return fmt; 
}  


var start = parent_job.getVariable("start_date");
var end = parent_job.getVariable("end_date");


start=new Date(start);
end=new Date(end);

start=start.getTime();
end=end.getTime();

if(start<=end){
	start=start+1*24*60*60*1000;
    parent_job.setVariable("start_date", new Date(start).format("yyyy/MM/dd"));
    true;
}else{
    false;
}

上面采用的是向mysql中插入数据,非常慢,可能会导致Kettle崩溃,可以选择另外一种方式,将“向mysql插入数据”组件换成写入文件,先将数据写入文件,后面在将文件写入hive,这一步可以使用hadoop copy组件或者执行sql(load加载)

发布了89 篇原创文章 · 获赞 79 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/L_15156024189/article/details/88746449