《Programming Hive》笔记

846 阅读17分钟

变量和属性

Hive 将参数、变量视为不同的命名空间。Hive 一共有 hiveconf,system,env 和 hivevar 4个命名空间。其中,env是不可修改的,它是shell里读取的环境变量。除了hivevar,其他的命名空间里的变量都要加上前缀来修改,比如:

set system:user.name=who;

传递配置参数的方式很多,比如命令行启动时传入,配置文件写入,或者是用set。但是,有些配置是在hive启动时生效的。比如:

$HIVE_HOME/bin/hive --hiveconf hive.root.logger=INFO,console

如果不知道某个配置,可以通过一种聪明的方法来获取(而不用查看配置文件):

hive -S -e "set" | grep warehouse 

Hive 模仿shell提供了一个.hiverc文件的设置。

REPL

在 Hive CLI 或者 Beeline 中,都是使用!来执行相关命令,比如!help查看帮助,或者执行 hdfs 和 shell 命令。不过,还有一些不是通过!执行的,比如ADD JAR,可以参考官方文档Hive Interactive Shell Commands

Hive 默认没有列名。如果想要在命令行打印表的列名:

set hive.cli.print.header=true;

数据类型

Hive 的字符串不需要指定长度。这一点用惯了关系数据库的人可能比较奇怪:

Note that Hive does not support “character arrays” (strings) with maximum-allowed lengths, as is common in other SQL dialects. Relational databases offer this feature as a performance optimization; fixed-length records are easier to index, scan, etc. In the “looser” world in which Hive lives, where it may not own the data files and has to be flexible on file format, Hive relies on the presence of delimiters to separate fields. Also, Hadoop and Hive emphasize optimizing disk reading and writing performance, where fixing the lengths of column values is relatively unimportant.

本质上,Hive 的数据类型和Java是一一对应的(因为最终是MR程序)。

这一段没看懂:

Note that you don’t need BINARY if your goal is to ignore the tail end of each record. If a table schema specifies three columns and the data files contain five values for each record, the last two will be ignored by Hive.

Hive 还支持集合数据类型,并且实际上是 Hive 在日常查询中最常用的部分。集合类型是反范式的,比如struct就明显违反1NF。范式本质上是为了减少冗余,同时避免增删改异常,而这对于数仓应用都是不必要的。这种反范式常见于NoSQL中。

读时模式

Hive 中一个非常重要的概念就是读时模式(Schema on read),和传统的数据库写时模式相对应。Hive 的设计目的是在MR上运行SQL,而不是构建一个数据库。通过读时模式,甚至不需要借助Hive本身就可以添加新的数据。

数据定义

可以通过extended或者formatted关键字(desc database不能使用)来查看更详细的信息。比如:

DESCRIBE DATABASE EXTENDED financials;

也可以查看表的某一列:

DESCRIBE mydb.employees.salary; 

Managed Tables & External Tables

其实按照国内一般的译法,应该叫托管表?这里大家更习惯叫做内部表。内部表是由Hive控制的,也就是说删除表会同步删除数据,而外部表是Hive不负责控制的,这是只有读时模式才能做到的。

可以通过前面的desc来查看表的类型。

修改表

Hive 允许对表的元数据做修改,但是不能修改数据。也就是说,我们不能修改表的LOCATION,但是可以修改表名,修改PARTITION信息,修改列信息等。书中举了一个从HDFS被分到S3的例子:

  • Copy the data for the partition being moved to S3. For example, you can use the hadoop distcp command:
    shell hadoop distcp /data/log_messages/2011/12/02 s3n://ourbucket/logs/2011/12/02
  • Alter the table to point the partition to the S3 location:
    shell ALTER TABLE log_messages PARTITION(year = 2011, month = 12, day = 2) SET LOCATION 's3n://ourbucket/logs/2011/01/02';
  • Remove the HDFS copy of the partition using the hadoop fs -rmr command:
    shell hadoop fs -rmr /data/log_messages/2011/01/02

需要注意的是,当手动修改分区信息之后,即使使用msck repair table也不能重新自动建立关系,因为数据已经相当于被移动了。

show parititions不能使用extended来得到每个分区的路径,只能通过DESCRIBE EXTENDED log_messages PARTITION (year=2012, month=1, day=2)这样的方式。

序列化

一般来说,我们都会使用 Hive 自带的几种文件格式,比如sequencefile,ORC。当我们想要使用一种自定义的序列化方法时,可以这样写:

CREATE TABLE kst 
PARTITIONED BY (ds string) 
ROW FORMAT SERDE 'com.linkedin.haivvreo.AvroSerDe' 
WITH SERDEPROPERTIES ('schema.url'='http://schema_provider/kst.avsc') 
STORED AS 
INPUTFORMAT 'com.linkedin.haivvreo.AvroContainerInputFormat' 
OUTPUTFORMAT 'com.linkedin.haivvreo.AvroContainerOutputFormat'; 

也可以事后增加新的SERDEPROPERTIES

ALTER TABLE table_using_JSON_storage 
SET SERDE 'com.example.JSONSerDe' 
WITH SERDEPROPERTIES ( 'prop1' = 'value1', 'prop2' = 'value2');

序列化的使用可以辅助我们解析不同的文件格式,比如 JSON,CSV 等。举个例子,假设现在数据文件是按照多个符号分割的,比如,,,可以使用org.apache.hadoop.hive.serde2.MultiDelimitSerDe

CREATE TABLE test (
 id string,
 hivearray array<binary>,
 hivemap map<string,int>) 
ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.MultiDelimitSerDe'                  
WITH SERDEPROPERTIES ("field.delim"="[,]","collection.delim"=":","mapkey.delim"="@");

Hive 还有一些无关痛痒的功能,比如利用HDFS的压缩(只对分区有效):

ALTER TABLE log_messages ARCHIVE PARTITION(year = 2012, month = 1, day = 1); 

防止分区被删除和查询:

ALTER TABLE log_messages PARTITION(year = 2012, month = 1, day = 1) ENABLE NO_DROP;
ALTER TABLE log_messages PARTITION(year = 2012, month = 1, day = 1) ENABLE OFFLINE;

数据操作

为什么LOAD DATA从本地就是拷贝,从HDFS上就是剪切?

The rationale for this inconsistency is the assumption that you usually don’t want duplicate copies of your data files in the distributed filesystem.

如果想要将同一张表的数据一次性插入到多个分区,可以这样写:

FROM staged_employees se 
INSERT OVERWRITE TABLE employees  
  PARTITION (country = 'US', state = 'OR')  
  SELECT * WHERE se.cnty = 'US' AND se.st = 'OR' 
INSERT OVERWRITE TABLE employees  
  PARTITION (country = 'US', state = 'CA')  
  SELECT * WHERE se.cnty = 'US' AND se.st = 'CA' 
INSERT OVERWRITE TABLE employees  
  PARTITION (country = 'US', state = 'IL')  
  SELECT * WHERE se.cnty = 'US' AND se.st = 'IL';

这是采用静态分区的方式。如果用动态分区的话更为简单。另外,也可以混用:

INSERT OVERWRITE TABLE employees 
PARTITION (country = 'US', state) 
SELECT ..., se.cnty, se.st 
FROM staged_employees se 
WHERE se.cnty = 'US';

Hive 默认动态分区是关掉的,这一点我认为相当不合理。另外,即使是打开动态分区,默认也是严格模式,即分区时必须包含一个静态字段。

This helps protect against a badly designed query that generates a gigantic number of partitions. For example, you partition by timestamp and generate a separate partition for each second! Perhaps you meant to partition by day or maybe hour instead. Several other properties are also used to limit excess resource utilization.

对于外部表而言,不能使用另一个表的查询结果来创建表。不过,可以使用LIKE创建表结构,然后用INSERT OVERWRITE插入。感觉这一点比较奇怪。

导出数据

除了直接拷贝HDFS之外,Hive 提供了类似于LOAD DATA的导出语句:

INSERT OVERWRITE LOCAL DIRECTORY '/tmp/ca_employees' 
SELECT name, salary, address FROM employees WHERE se.state = 'CA'; 

还有一种方法是使用export table导出表。这里书中似乎没有提到(在 Hive 0.8引入)。

查询

正则

除了大家都知道的LIKERLIKE,Hive 其实还提供了列名的正则匹配。

SELECT symbol, `price.*` FROM stocks; 

不过,书中是基于早期版本的,现在要使用这一feature需要设置:

set hive.support.quoted.identifiers=none;

这个配置是 Hive 用来支持任意列名的,只要通过``指定。默认值是column

Map端聚合

这个选项默认是打开的,不过,Map端如何实现聚合呢?它是一个基于哈希的部分聚合/第一层聚合(first-level aggregation)。这个功能不是通过Combiner实现的,不过和这个很类似。Hive 是在Mapper中维护了一个哈希表,然后把相同的key直接聚合到一起。

除了hive.map.aggr之外,网上大家常说的还有一种:

set hive.groupby.skewindata = true;

这个是把 Hive 的 groupby 划分为两个阶段,在第一个阶段的 reduce 只做部分聚合,从而达到负载均衡的目的。

本地模式

Hive 的某些查询不需要使用MR,比如SELECT * FROM table。我们知道hive.exec.mode.local.auto可以设置尽可能进行本地模式执行,不过,它控制的是其他可能不需要MR的语句,对于前面这种查询,即使该选项没有打开,也会使用本地模式。所以,不能把这个配置和本地模式直接等价。

实际上,SELECT * FROM table是由hive.fetch.task.conversion控制的。如果设置成none,将使得所有查询使用 MR 进行处理。

除了书中所说的这个开关,还可以配置:

--默认为134217728,即128M
set hive.exec.mode.local.auto.inputbytes.max=50000000;
--默认为4
set hive.exec.mode.local.auto.input.files.max=5;

谓词运算符

关于谓词运算符有几点比较有意思的地方。第一,Hive支持对空值进行比较,不过要使用特殊的相等运算符<=>。第二,Hive 既可以使用<>,也可以使用!=

Hive 中关于浮点数的比较也受到IEEE 754规范的影响。比如:

hive> SELECT name, salary, deductions['Federal Taxes']    
    > FROM employees WHERE deductions['Federal Taxes'] > 0.2; 
John Doe        100000.0        0.2 
Mary Smith      80000.0         0.2
Boss Man        200000.0        0.3 
Fred Finance    150000.0        0.3 

这是因为0.2的类型是double,而deductions['Federal Taxes']的类型是float。在不同精度的浮点数中,0.2表示的实际值是不同的(由于浮点数不能精确表示一个数字)。

连接

我们都知道,Hive 不支持非等值连接。不过这是为什么呢?因为 MR 实现 JOIN 的基本原理很简单,就是相同的 key 丢到一个 reduce 里。如果是非等值连接,意味着其中一个表的每个key,都要广播到所有的 reduce 里(除了它本身的值)——这是 MR 所不支持的。另外,Hive 也不支持在连接条件上使用OR,原因类似。

如果使用多表连接,当连接条件都是针对同一个键时,Hive 可以将 MR 任务优化为一个。

书中还提到,外连接会忽略掉分区条件(where语句),相反内连接就不会。不过,这应该属于谓词下推可以优化的范畴。

其实 Hive 实现 JOIN 的方法其实非常多,上面只是其中的一种——COMMON JOIN。书中提到了一个 Hive 以前的优化策略:把小表放在前面。这是因为 Hive 过去使用 MAP JOIN 时,默认从左向右解析,并且左边是小表。这个功能由配置决定:

set  hive.auto.convert.join=true;

但是这一点在后面的版本已经不存在了,因为优化器可以判断谁是小表,只需要启动一个 MapTask 来比较所有表的大小即可。不过,所有的小表都不能超过hive.mapjoin.smalltable.filesize

MAP JOIN 的原理是,创建一个哈希表来保存小表的键值(通过DistributedCache,可以把这个哈希表变为全局共享)。

BUCKET MAP JOIN

如果表是分桶的,还可以进一步做优化。只要 JOIN 的条件就是分桶的条件,且两个表的分桶数量是比例关系,就可以使用 BUCKET MAP JOIN。因为分桶可以保证键值相同的一定会出现在一个桶内,所以每次只需要相对应的桶即可。另外,如果两个表使用了SORT BY,即桶内排序,且桶数相同,就可以直接使用归并,也就是 SORT MERGE BUCKET JOIN(SMB JOIN)。

Hive 需要开启该配置:

set hive.input.format=org.apache.hadoop.hive.ql.io.BucketizedHiveInputFormat; 
set hive.optimize.bucketmapjoin=true; 
set hive.optimize.bucketmapjoin.sortedmerge=true;
set hive.auto.convert.sortmerge.join=true;

最后一个配置是允许 SMB JOIN 转化为 Map-side JOIN。换句话说,SMB JOIN 并不一定是 Map-side JOIN。

还有一组看起来很相似的配置:

hive.enforce.bucketmapjoin=false
hive.enforce.sortmergebucketmapjoin=false

这个在HIVE-3289patch里,是为了report相关的优化是否启动。举个例子,假设该配置为 true,而对于 mapjoin 没有转化为 bucketmapjoin,就会抛出异常。

SKEW JOIN

JOIN 操作可能会导致数据倾斜,这一点书中由于出版较早没有提到。可以配置:

set hive.optimize.skewjoin=true;
set hive.skewjoin.key=500000;
set hive.skewjoin.mapjoin.map.tasks=10000;
set hive.skewjoin.mapjoin.min.split=33554432;

SKEW JOIN 的优化策略是将倾斜的键转化为 MAP JOIN 处理。另外,还可以配置在编译期识别倾斜键:

set hive.optimize.skewjoin.compiletime=true

识别方法是在表的元信息中增加倾斜信息:

CREATE TABLE <T> (SCHEMA) SKEWED BY (keys) ON ('c1', 'c2') [STORED AS DIRECTORIES];

LEFT SEMI JOIN

这个在目前的关系数据库都是实现为IN或者EXISTS。不过 Hive 不支持这样写,而是通过LEFT SEMI JOIN(有点原教旨的感觉)。LEFT SEMI JOIN最大的好处是first match,也就是在第一次找到了匹配之后就返回,而不再继续扫描后续的行。这意味着输出结果是可预测的(不会超过左表的行数)。

distribute by

为什么要保证按照某个键划分 reducer 呢?因为默认不指定的话,它分发的 reducer 是不确定的——很有可能是文件行号。

范式设计

这一部分就是体会 Hive 设计思想的地方了。

分区

分区并不是越细越好。分区过多会产生许多小文件,而小文件的缺陷对于 HDFS 太多了。比如,它会增加 namenode 的负担;如果不额外设置,小文件会导致 MR 的 Task 过多,引起频繁的资源分配和 JVM 启动和销毁(如果没有开启重用的话)。在实际工作中,我还遇到过小文件过多导致磁盘很快用完的情况,因为 Hive 会给每个分区下的数据放满一个文件块,哪怕其中的数据本身并没有达到128M。

分桶

如果指定了hive.enforce.bucketing=true,那么 Hive 可以自动根据表的桶数设置 reducer。

唯一键和标准化

由于 Hive 用于数仓,使用的是星型模型,是反范式的。反范式除了我们知道的好处之外,书中还提到了可以优化磁盘读取的速度,因为我们总是连续读取的。

Hive 没有自增的主键。对于我们熟悉的数据仓库理论,事实表和维度表是根据外键关联的,因此这个关联必须通过关系数据库提前保证。

使用列式存储

本书中举的例子是 RCFile。不过,现在 Hive 主流的都是 ORC 格式了,这是一个基于 RCFile 的优化格式。列式存储的好处有一点就是具有优良的压缩性。

Key Default Notes
orc.compress ZLIB high level compression (one of NONE, ZLIB, SNAPPY)
orc.compress.size 262,144 number of bytes in each compression chunk
orc.stripe.size 67,108,864 number of bytes in each stripe
orc.row.index.stride 10,000 number of rows between index entries (must be >= 1000)
orc.create.index true whether to create row indexes
orc.bloom.filter.columns comma separated list of column names for which bloom filter should be created
orc.bloom.filter.fpp 0.05 false positive probability for bloom filter (must >0.0 and <1.0)

可以使用STORED AS orc tblproperties ("orc.compress"="NONE")创建一个无压缩的 orc 表。同样,也可以指定使用 Snappy。

所谓列式存储并不代表一列都放在一起,而是和 ORC 的名字一样,Row Columnar——先按照多行聚集,然后在集合中进行列式存储。这个行数往往是非常大的,ORC 中把它叫做一个 Stripe。

实际上,Store as是一个简写,它相当于指定了InputFormatOutputFormatSerDe。可以用DESC EXTENDED TABLE查看表对应的这三个属性。

还有一种列式文件格式是 Parquet。它是 Cloudera 公司开发的(今年大数据公司已经凉凉了)……

总是使用压缩

我记得以前使用社区版 Hadoop 的时候,为了使用 Snappy 压缩颇费周折。原因是社区版 Hadoop 使用的是 Apache 协议,默认没有包含 Snappy(Snappy 使用的是 GPL 协议)。

目前 Snappy 使用的应该是最多的,它唯一的缺点可能就是不支持分割了。

可以在core-site.xml里的io.compression.codecs配置新的压缩格式。

调优

Explain

比如 GROUP BY,我们可以看到逻辑执行计划中也许会有 Map 端的 GROUP BY Operator。

Limit Tuning

这个很有意思,我还没有用过。通过设置hive.limit.optimize.enable为 true,可以将 LIMIT 设置为随机采样。还可以配置:

<property>
  <name>hive.limit.row.max.size</name>
  <value>100000</value>
  <description>When trying a smaller subset of data for simple LIMIT,
how much size we need to guarantee each row to have at least. </description>
</property>
<property>
  <name>hive.limit.optimize.limit.file</name>
  <value>10</value>
  <description>When trying a smaller subset of data for simple LIMIT,
maximum number of files we can sample.</description> 
</property>

Optimized Joins & Local Mode

Parallel Execution

设置hive.exec.parallel可以让某些 Stage 并行执行。

Strict Mode

Hive 的严格模式是出于性能考虑的。即:

  • 限制分区表查询不使用分区条件
  • 限制 ORDER BY 不使用 LIMIT
  • 限制使用笛卡尔积

JVM Reuse

这个在 MR 里用的比较多,可以防止小文件过多时 JVM 频繁创建和销毁的开销。不过也有缺点,因为它的原理是多占用一段时间分配给自己的container(书中说的是 Slot,这个是MR v1里的),有可能导致资源囤积。

Tuning the Number of Mappers and Reducers

Hive 中 Mapper 和 Reducer 的个数都依赖于数据量。Mapper 的不用说,和 MR 是相同的,受到块大小和分片大小的约束;Reducer 除了可以直接设置以外,还可以根据hive.exec.reducers.bytes.per.reducer来计算。另外,这个个数不能超过hive.exec.reducers.max

不过,有时候你无法设置 reducer 的个数。这个用过COUNT(DISTINCT)的人应该有体会,因为这会被翻译成一个 reduce 任务来计算,性能不够好。有一种方法是使用GROUP BY代替 DISTINCT。我在想这样 DISTINCT 存在的必要是什么呢?

Dynamic Partition Tuning

前面说过动态分区可以设置hive.exec.dynamic.partition.mode为严格模式来保证插入时至少有一个静态分区。

动态分区一次性创建的分区是有限制的,这可以通过hive.exec.max.dynamic.partitionshive.exec.max.dynamic.partitions.pernode修改。

还有一个dfs.datanode.max.xcievers可以配置 datanode 打开的文件句柄数。这个一般在 Hadoop 里都会配置。

Speculative Execution

如果数据量很大而导致 Mapper 和 Reducer 很慢的话,建议把这个功能关掉,因为它的开销很大。

Virtual Columns

比如INPUT__FILE__NAMEBLOCK__OFFSET__INSIDE__FILE。在 Hive 0.8新增了这些列:

  • ROW__OFFSET__INSIDE__BLOCK

  • RAW__DATA__SIZE

  • ROW__ID

  • GROUPING__ID

注意这些都是双横杠……

函数

UDF

自定义一个 UDF 也很简单,只需要继承UDF即可。UDF类采用的不是接口的约束,而是基于约定的,因此我们自定义函数类的evaluate的返回类型可以随意定义。注意这个类已经是 deprecated 了,现在推荐GenericUDF,也就是基于接口的约束(实现抽象方法)。GenericUDF虽然名字叫这个,但却不是基于泛型的,evaluate返回类型被定死在 Object 上。

实际上,书中没有提到 Hive 目前支持用反射调用 Java 函数。这比我们自己手写许多函数要方便许多,而原本许多 UDF 都是直接基于 Java 函数实现的。

UDAF

实现一个用户聚合函数比 UDF 稍微麻烦一些。有一点需要注意的是,UDAF 的实现必须支持分治,即数据被任意划分计算而不影响结果,这一点类似于 ForkJoin 这样的结构,因为 Hive 的聚合可以实现为 MAP GROUP BY。

实现一个 UDAF 可以继承UDAF或者AbstractGenericUDAFResolver,不过这两个类目前都是 deprecated 了。更加尴尬的是,你会发现注释中目前所有相关的接口和抽象类全是 deprecated,包括GenericUDAFResolver2GenericUDAFResolver

实现GenericUDAFResolver2只需要实现一个getEvaluator方法。它的参数是传入聚合函数的所有参数的类型,返回值是一个抽象类GenericUDAFEvaluator,这个才是 UDAF 实现的核心。

前面说过,UDAF 必须支持分治,也就是这里的terminatePartialterminatePartialiterate用于 map 端,而mergeterminate在 reduce 端。

UDTF

表生成函数有一个特点,就是不能使用其他的列。这一点用过的人应该都很有体会,使用explode拆解集合类型时,如果需要其他列必须使用 LATERAL VIEW。这里其他列和表生成函数实际构成了一个笛卡尔积的关系。

自定义 UDTF 要比 UDAF 简单许多,只需要 override 一个process方法,将传入的对象解析,然后对每行调用forward方法(在基类里)即可。具体写法可以参考GenericUDTFParseUrlTuple,其实现非常清晰。

在 UDTF 和 UDAF 中,会大量使用ObjectInspector的各种继承,比如StringObjectInspector。这些全部都是接口,而它的实例是通过运行时传入的。ObjectInspector的作用是控制对数据的解析和访问。它没有实现在 Serde 里,而是单独抽出来使得 Serde 更加简洁。ObjectInspector还可以解耦 Hive 的 Operator 和底层的 Hadoop Writable,使得不同的 Operator 可以使用不同的序列化方式。

Window Function

一般术语叫做窗口函数,有时候也叫做分析函数。在关系数据库和 SQL 规范中,这是一个非常常用的概念。本书很神奇的没有介绍它们(虽然在示例中出现了)。这个具体用法参考一般的关系数据库即可,需要注意的是像 ROLLUP 的功能需要略高一些的版本(>=0.10.0)。


To be continued...