我们知道Hive是一个构建在MapReduce之上并提供了SQL语法的查询分析引擎。虽然Hive可以处理巨量的数据,但是不同的优化手段会在处理时间上产生很大的差异。
在Hive中,可以从以下几个方面进行优化:
- 分区partition
- 分桶bucket
- 使用Spark作为执行引擎
- 使用压缩
- 使用ORC格式
- join优化
- 基于CBO的优化
在对表基于分区列进行分区之后,写入表的数据会存放在不同的目录下,但是分区列的数据是不存储在目录下的文件中的。当通过分区列进行查询表数据的时候,只会扫描指定分区的数据,其他的分区的数据是不会被查询的,这就减少了磁盘I/O时间,从而提升了查询性能。
分区表的创建:
注意:分区列是定义在partitioned by后面的,并非字段列表中。
分区又分为静态分区和动态分区两种。
静态分区是指我们提前就知道要插入到表中数据的分区,比如下面语句中的分区列country:
静态分区是非常不方便的,因为要执行多个语句将不同分区的数据插入到对应的分区中。
动态分区不用指定分区的值,而是在插入数据的过程中,自动创建分区。比如下面语句中的分区列country:
Hive中启用动态分区(Hive默认值就是true):
另外,动态分区也有两种模式:
- Strict模式:分区列中至少有一个是静态分区列。
- Non-strict模式:分区列可以全部为动态分区列。
切换动态分区模式(Hive中动态分区模式默认为strict):
Hive表中,每个分区对应一个目录,而每个分桶对应一个文件。
创建分桶表语句:
Hive是通过在某一列上应用hash函数将数据分为指定数量的文件,bucket的个数由公式hash_function(bucketing_column) mod num_buckets,其中的哈希函数主要依赖于分桶列的数据类型。对于int类型,hash_int(i) == i,比如说int类型的user_id,以0结尾的user_id会划分到第1个bucket,以1结尾的user_id会划分到第2个bucket。 分桶也是为了提高查询效率。
默认情况下,Hive底层是基于MapReduce,而MapReduce计算过程是基于磁盘的,这也就是预示着任务的执行会比较耗时。那么,这时我们就可以替换底层执行引擎,用基于内存计算的Spark。
查询时指定执行引擎(Hive中默认执行引擎为mr,即hive.execution.engine=mr):
或者在hive-site.xml中配置默认执行引擎。
我们知道Hive中的查询很多会涉及到大量的磁盘I/O和网络I/O,由此想到,如果我们能减少查询数据大小的话,就可以提升查询效率。那如何减少数据的大小呢?答案就是对数据进行压缩。此外,压缩也能减少磁盘的占用。
Hive输出启用压缩(默认是未启用压缩的):
但是,也不能一味的追求压缩率,因为高压缩率会导致解压时消耗更多的CPU,因此我们要在压缩和解压缩之间取得平衡。常用的压缩格式有snappy、bzip2、gzip等,Spark中默认的压缩格式就是snappy。
选择合适的文件格式可以在很大程度上提升任务执行的效率。Hive支持很多的文件格式:Text、Parquet、ORC、Avro、Sequence等,甚至还可以自定义文件格式。
当我们读取Text格式表中的某一列的时候,必须要把一整行数据都读出来。而如果我们将表设置为列式存储格式(比如Parquet和ORC),我只需查询所需要的列,不需要的列是不会扫描的。
ORC格式在Hive中以下的优化点:
- 列式存储可以实现更高的压缩率(Hive在使用ORC格式时默认的压缩格式为ZLIB)
- ORC可以使用存储在文件中的轻量级索引,跳过不必要的记录扫描
- 如果Block中的行记录与查询无关,则可以跳过这些行,不进行解压缩
实际上,ORC存储格式既是面向行的,又是面向列的。一个ORC格式的文件一系列的stripes组成的,每个stripe中,列是被分别压缩的,每个stripe由以下3部分组成:
- Stripe footer:存放文件级和stripe级的统计信息,以确定是否需要读取文件的剩余部分。
- Row data:默认包含10000行数据。
- index data:包含每一列的最大、最小值,以及每一列所处的行的位置信息。
使用ORC格式可以用到以下优化手段:
-
查询下推
-
布隆过滤器
-
ORC压缩
-
向量化执行
使用向量化的查询执行,可以在很大程度上优化scan、filter、aggregation、join等操作。标准的查询执行一次只处理一行,而且会 走过很长的代码调用和元数据解释。而向量化查询执行每次可以处理1024行,每一行会被保存为一个向量。对于向量来说,简单的算术和比较运行可以很快的完成。执行树被加快是因为,在相同数据类型的数据块中,编译器可在紧凑循环中生成代码来执行相同的函数,而不必遍历独立函数调用所需的长代码路径。这种类似SIMD的行为带来了很多好处:执行的指令更少、缓存行为更好、改进的管道、更有利的TLB行为,等等。
可通过以下配置使用向量化查询执行:
在了解Join优化之前,我们首先得知道join查询是如何转换成MapReduce进行执行的:
- Mapper端会先基于join key并行地对表数据进行排序
- 排序后传递给Reducer端,具有相同key的tuple会被传递给同一个Reducer,但是一个Reducer可能会处理多个key对应的tuple。tuple中也会包含参与join的表的id,标识这一条数据是属于哪个表的。
join的每个map/reduce阶段,join序列中的最后一个表会默认作为流表,其他的表会缓存起来。因此,一般会将最大表放到一系列join操作的最后一个表,这样可以减少内存占用。或者我们也可以显示的指定流表:
另外一个当我们在使用join的时候要注意的是,首先进行join,然后对join结果再执行where条件进行过滤,例如下面的查询语句:
针对上面的问题,我们可以将过滤条件提前执行,以减少不必要数据的查询:
由于join操作会涉及到排序和shuffle操作,而这些操作往往产生大量的IO操作,造成任务运行耗时较长。针对这些问题,Hive中使用了以下join优化算法:
- Multi-way Join
- Map Join
- Skew Join
如果多个join操作共享同一个join key,那么所有这些join操作可以由一个reduce task来执行,减少了reduce的数量,就比如下面的SQL:
这种join优化算法非常适合类似星型模型中小维表和大事实表进行join的场景,它会将小表加载到所有mapper端的内存中,大表作为流表,在map端即可完成join操作,不会产生昂贵的shuffle的操作。
通过以下配置,Hive会自动优化哪些适合Map端join的join场景:
或者我们也可以显示地指定:
对于出现数据倾斜的情况,Hive会自动进行优化,拆分成多个子任务,最后再将子任务的结果进行合并。相关的配置:
Hive使用硬编码查询计划来执行查询,直到出现了基于代价的优化模型(cost-based optimizer, CBO),CBO通过搜集元数据信息来优化查询计划,它提供了两种类型的优化:逻辑优化和物理优化。