Hive 笔记

Posted by Fioncat on April 22, 2018

Hive是基于Hadoop的一个数据仓库工具。可以将结构化数据映射为一张数据库表。并提供类似SQL的HiveSQL(HQL)进行数据查询等功能。Hive底层将HQL转换为MapReduce任务来操作HDFS中的数据。

利用Hive,可以快速实现MapReduce功能。而不必编写MapReduce程序。

如果不会编写Java程序,又想使用MapReduce来处理大数据,就可以使用Hive。

注意Hive不能作为一般数据库使用(无事务,有大量自建冗余),也不适合实时数据分析,无法进行行级别的增删改(受Hadoop限制),是一款离线数据分析工具。

Hive 安装

Hive下载的时候记得要选择和Hadoop版本符合的版本。

Hive安装前必须保证机器上已经提前安装了jdk和Hadoop。并且配置好了环境变量(JAVA_HOME, HADOOP_HOME)。在下面的例子中,我的机器中已经安装了Hadoop 2.7.1伪分布式环境和jdk 1.8_65。

我将会在CentOS6.5中安装hive 1.2.0作为示例。解压hive的tar包:

1
2
3
[root@hadoop1 ~]# tar xzf apache-hive-1.2.0-bin.tar.gz -C /usr/develop/
[root@hadoop1 ~]# cd /usr/develop/
[root@hadoop1 develop]# mv apache-hive-1.2.0-bin/ hive1.2

如果你的系统中有”HADOOP_HOME”这个环境变,Hive就可以直接使用了。,如果没有,可以到/etc/profile下增加这个环境变量。

通过hive目录下的bin中的hive程序就可以直接执行Hive了:

1
2
3
4
5
6
7
8
9
10
[root@hadoop1 develop]# cd hive1.2/
[root@hadoop1 hive1.2]# ls
bin  conf  examples  hcatalog  lib  LICENSE  NOTICE  README.txt  RELEASE_NOTES.txt  scripts
[root@hadoop1 hive1.2]# cd bin/
[root@hadoop1 bin]# ls
beeline  ext  hive  hive-config.sh  hiveserver2  metatool  schematool
[root@hadoop1 bin]# ./hive

Logging initialized using configuration in jar:file:/usr/develop/hive1.2/lib/hive-common-1.2.0.jar!/hive-log4j.properties
hive>

这就直接进入了Hive的命令行界面,表示安装成功了。

HiveSQL

HiveSQL的基本使用和标准SQL很像。支持:

  • SHOW DATABASES;
  • USE db;
  • SHOW TABLES;
  • DROP DATABASE db;
  • DROP TABLE db;

HiveSQL创建表的命令也和sql很像:

  • CREATE TABLE table(field type, field type, …) [row format delimited fields terminated by ‘xx’];

Hive可以使用的类型:

Hive类型 java类型
TINYINT byte
SMALLINT short
INT int
BIGINT long
BOOLEAN boolean
FLOAT float
DOUBLE double
STRING String
TIMESTAMP Timestamp
BINARY byte[]

后面的row format…的作用是指定数据源的分隔符。这在从外部导入数据的时候特别有用。

Hive创建database实际上就是在HDFS上创建一个/user/hive/warehouse/xxx.db的目录。在database中创建table实际上就是在这个目录中创建一个和表名一样的目录。目录中储存了各种数据。

Hive默认有一个default数据库。默认的default目录是Hive的根目录。

一般来说,Hive不支持行级别的增删改。但是Hadoop 2.x后引入了追加操作。所以新的Hive支持插入操作:

  • INSERT INTO table(field, field, field, …);

但是我们更多的情况是把外部的数据通过Hive导入HDFS。这使用:

  • load data local inpath ‘path’ into table table_name;

这会把这个文件放到HDFS中表的目录下。Hive所有针对表的查询实际上就在在表目录下对各种数据文件的查询。

下面看一个导入外部数据的简单的例子:

我们在外部创建一个文本文件,作为数据源使用:

1
2
3
4
5
6
[root@hadoop1 bin]# vim /home/data/stus.txt
1|zhang|male
2|li|female
3|pa|male
4|lili|male
5|mk|female
注意我们使用” “作为每个数据的分隔符,需要告诉Hive我们使用的分隔符,在创建表的时候,我们加上format即可。
hive> create table student(id int, name string, gender string)row format delimited fields terminated by '|';
OK
Time taken: 0.508 seconds

随后导入数据,查询导入的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
hive> load data local inpath '/home/data/stus.txt' into table student;
Loading data to table test.student
Table test.student stats: [numFiles=1, totalSize=59]
OK
Time taken: 0.402 seconds
hive> select * from student;
OK
1   zhang   male
2   li  female
3   pa  male
4   lili    male
5   mk  female
Time taken: 0.257 seconds, Fetched: 5 row(s)

Hive 切换元数据库

除了我们要真实储存的数据,Hive还需要储存一些元数据。例如,我们创建了哪些数据库,数据库中有哪些表等。

Hive把元数据储存到一个关系型数据库中,那么这个数据库就是Hive的元数据库。在默认情况下,Hive使用内置的Derby数据库储存元数据。

Derby会在执行Hive的地方创建一个derby.db保存数据。所以如果你使用Derby,就表示你必须一直在一个目录下操作Hive。一旦切换目录,Hive的元数据会丢失。

下面是使用Derby丢失元数据的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@hadoop1 bin]# ./hive

Logging initialized using configuration in jar:file:/usr/develop/hive1.2/lib/hive-common-1.2.0.jar!/hive-log4j.properties
hive> show databases;
OK
default
park
test
Time taken: 0.799 seconds, Fetched: 3 row(s)
hive> exit;
[root@hadoop1 bin]# cd ..
[root@hadoop1 hive1.2]# bin/hive

Logging initialized using configuration in jar:file:/usr/develop/hive1.2/lib/hive-common-1.2.0.jar!/hive-log4j.properties
hive> show databases;
OK
default
Time taken: 0.809 seconds, Fetched: 1 row(s)

我们看到一旦切换到Hive的根目录下执行Hive,之前创建的数据库就全部丢失了。

这样储存元数据的方式显得不太人道。除了Derby,Hive还支持使用MySQL作为元数据库。

首先,需要在Linux中安装MySQL。

在准备好MySQL之后,进入conf目录,使用Hive的模板配置文件更名为hive-site.xml:

1
2
3
4
5
[root@hadoop1 hive1.2]# cd conf/
[root@hadoop1 conf]# ls
beeline-log4j.properties.template  hive-env.sh.template                 hive-log4j.properties.template
hive-default.xml.template          hive-exec-log4j.properties.template  ivysettings.xml
[root@hadoop1 conf]# cp hive-default.xml.template hive-site.xml

这个配置文件中有很多默认的配置项。我们关心的是如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<property>
    <name>javax.jdo.option.ConnectionURL</name>
    <value>jdbc:mysql://hadoop1:3306/hive?createDatabaseIfNotExist=true</value>
</property>

<property>
    <name>javax.jdo.option.ConnectionDriverName</name>
    <value>com.mysql.jdbc.Driver</value>
</property>

<property>
    <name>javax.jdo.option.ConnectionUserName</name>
    <value>xxx</value>
</property>

<property>
    <name>javax.jdo.option.ConnectionPassword</name>
    <value>xxx</value>
</property>

其中,分别配置了数据库url,驱动程序,连接的用户名和密码。应该根据实际情况进行配置。

注意需要把MySQL的驱动文件放到hive的lib目录下。

注意一点,Hive的元数据库的编码集必须是latin1。如果不是,会出现奇怪的问题。

以上都没有问题的话,就能够正常使用Hive了。

内部表和外部表

Hive中默认建立的是内部表。这种表一旦被建立,就会在HDFS中存在有目录。把数据写入表的过程实际上就是把数据写入HDFS的过程。

内部表的特点是:先有表,后有数据。

但是还有一种情况:数据已经在HDFS中存好了,我们只是需要创建一张表去关联这些已经存在的数据。

外部表的特点是:现有数据,后有表。

下面做一个演示,先向HDFS中上传数据:

1
2
3
4
5
6
7
8
[root@hadoop1 bin]# more /home/data/persons
zhang|18
li|20
wang|28
zhao|30
qian|20
[root@hadoop1 bin]# hadoop fs -mkdir /person
[root@hadoop1 bin]# hadoop fs -put /home/data/persons /person/data

可见我们将数据上传到了/person/data中。

接下来,创建外部表,让这张表链接上这些数据,语法是:

  • create external table table_name(fields…) [row format delimited fields terminated by ‘x’] location ‘path’;

和内部表的区别是多了external和location。location用来表示数据的位置。

1
2
3
4
5
6
7
8
9
10
11
hive> create external table person(name string, age int)row format delimited fields terminated by '|' location '/person';
OK
Time taken: 0.218 seconds
hive> select * from person;
OK
zhang   18
li  20
wang    28
zhao    30
qian    20
Time taken: 0.336 seconds, Fetched: 5 row(s)

在这种情况下,就不会到/user/hive/db下创建表文件夹了。

内部表和外部表的另外一个区别是,如果删除内部表,那么HDFS中对应的文件夹将会被删除。而删除外部表,HDFS中对应的文件夹则不会被删除。

分区表

Hive也支持分区表,也就是可以对数据进行分区操作,从而提高查询的效率。

分区表需要一个分区字段。对于字段的不同值,会储存在不同的目录下。

创建分区表的语法为:

  • create table table_name(fields) partitioned by(field type) [row format delimited fields terminated by ‘x’];

其中,partitioned by指定分区的字段。

例如,我们现在有两个不同的文件,分别表示1月份和2月份的学生考试成绩:

1
2
3
4
5
6
7
8
[root@hadoop1 bin]# cat /home/data/score/1
zhang 70
li 90
wang 78
[root@hadoop1 bin]# cat /home/data/score/2
zhang 98
li 86
wang 79

我们希望在建表的时候按照月份进行分区,建表如下:

1
2
3
hive> create table score(name string) partitioned by(month int) row format delimited fields terminated by ' ';
OK
Time taken: 0.371 seconds

在加载数据的时候,需要附带这份数据的分区信息:

1
2
3
4
5
6
7
8
9
10
hive> load data local inpath '/home/data/score/1' into table score partition(month=1);
Loading data to table park.score partition (month=1)
Partition park.score{month=1} stats: [numFiles=1, numRows=0, totalSize=23, rawDataSize=0]
OK
Time taken: 0.861 seconds
hive> load data local inpath '/home/data/score/2' into table score partition(month=2);
Loading data to table park.score partition (month=2)
Partition park.score{month=2} stats: [numFiles=1, numRows=0, totalSize=23, rawDataSize=0]
OK
Time taken: 0.337 seconds

partition指明了上传数据的分区信息。

在使用的时候,操作和一般表差别不大,但是使用分区字段进行限制的话,查询效率会提升很多:

1
2
3
4
5
6
7
8
9
10
11
12
hive> select * from score where month=1;
OK
zhang   1
li  1
wang    1
Time taken: 0.386 seconds, Fetched: 3 row(s)
hive> select * from score where month=2;
OK
zhang   2
li  2
wang    2
Time taken: 0.083 seconds, Fetched: 3 row(s)

在使用分区表的时候,查询语句应该尽量都加上分区字段作为限定条件。

Hive底层实际上就是把分区表保存为多个文件夹。然后在元数据库中记录这些文件夹下的位置。

分区信息可以有多个,在实际情况中,发现如果经常需要按照某个字段进行查询并且数据量非常大,则可以把这些字段设置为分区字段。

分桶表

分桶表对于数据的区分比分区表更加细粒度。分桶的原理就是根据某列数据进行Hash算法转换后储存在不同的区域。在查询的时候,如果对分桶列进行了限定,那么就会根据限定数据的Hash值到对应的区域中去查询数据。

分桶的一个重要作用是,在数据量非常大的时候,可以利用分桶筛选部分数据进行测试。

分桶和分区是可以放在一起使用的。

创建分桶表的语法是:

  • create table table_name(fields) clustered by (field) into # buckets …;

clustered by指明了按照哪个字段进行分桶。#表示分成几个桶。

注意在默认情况下,Hive的分桶是关闭的,需要执行以下语句打开Hive的分桶功能:

  • set hive.enforce.bucketing=true;

分组表无法使用load data。这样不会进行真正的分桶。

在实际中,我们一般创建一个测试表,在测试表中添加分桶功能。把真正的数据表的数据导入测试表,这样Hive就会进行分桶操作。

从一个表将数据导入另一个表的语法是:

  • insert overwrite into table target_table select * from source_table;

这个导入的过程需要MapReduce的参与才可以把数据分开。

下面我们做一个演示,为之前的student表创建一个辅助的分桶表:

1
2
3
4
5
6
hive> create table student_temp(id int, name string) clustered by (id) into 2 buckets row format delimited fields terminated by '|';
OK
Time taken: 0.087 seconds
hive> set hive.enforce.bucketing=true;
hive> insert overwrite table student_temp select * from student;
...

我们将student_temp按照id分为了2桶。在HDFS底层实际上就是分文件夹进行存放。

这样在对id进行查询的时候会根据查询数据的Hash值到对应的目录下去查询。

如果要根据桶对数据进行取样,可以使用下面的语法:

  • select xx from xx where xx tablesample(bucket # out of # on field);

其中,tablesample就表示取样,bucket后的数字表示取出第几个桶,out of后的数字表示一共有几个桶,field表示分桶字段。

下面取出第2个桶:

1
2
3
4
5
6
hive> select * from student_temp tablesample(bucket 2 out of 2 on id);
OK
5   mk
3   pa
1   zhang
Time taken: 0.352 seconds, Fetched: 3 row(s)

一共有5个数据,我们取出了其中的3个,这就实现了分桶。

UDF

Hive提供了很多内置函数(具体请参阅API文档),我们可以使用这些函数来加快开发效率。

当然,我们也可以自定义一个函数供我们使用。

我们首先需要把相关的包导入到我们的项目中。

我们开发一个将字符串转换为大写的功能,这需要我们新建一个继承自UDF的类,并且自己写一个evaluate方法:

1
2
3
4
5
6
7
8
9
package cn.lazycat.bdd.hive.udf;

import org.apache.hadoop.hive.ql.exec.UDF;

public class MyToUpper extends UDF {
    public String evaluate(String str) {
        return str.toUpperCase();
    }
}

下面把项目打包成jar包,并在Hive中执行addJar命令将jar包传递给Hive。并创建自定义函数。

1
2
3
4
hive> add jar /root/MyToUpperCase.jar;
Added [/root/MyToUpperCase.jar] to class path
Added resources: [/root/MyToUpperCase.jar]
hive> create temporary function MyToUpper as 'cn.lazycat.bdd.hive.udf.MyToUpper';

随后就可以像使用正常函数一样使用自定义函数了。

Hive JDBC 编程

Hive实现了JDBC标准,我们可以使用JDBC来间接操作Hive。

在使用前,我们需要启动Hive的服务:

1
[root@hadoop1 hive1.2]# bin/hive --service hiveserver2 &

加上”&”可以防止这个程序一直占据控制台。

随后开发客户端,需要导入hive-jdbc这个包,并且编写JDBC代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package cn.lazycat.bdd.hive.jdbc;

import java.sql.*;

public class HiveJDBCDemo {

    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        Class.forName("org.apache.hive.jdbc.HiveDriver");
        Connection conn = DriverManager.getConnection(
                "jdbc:hive2://192.168.117.51:10000/park");
        PreparedStatement statement = conn.prepareStatement("SELECT * FROM person");
        ResultSet resultSet = statement.executeQuery();

        while (resultSet.next()) {
            System.out.print(resultSet.getString(1) + "\t");
            System.out.println(resultSet.getString(2));
        }

        conn.close();
    }

}

终端输出:

1
2
3
4
5
zhang	18
li	20
wang	28
zhao	30
qian	20