1. 为什么你的Hive查询慢得让人想砸键盘
如果你在大数据团队待过,大概率经历过这种场景:一个看起来逻辑很简单的Hive SQL,跑起来却要几十分钟甚至几个小时,日志刷得飞快但进度条纹丝不动。更让人崩溃的是,同样的逻辑换一种写法,时间直接砍掉一大半。这不是玄学,而是Hive执行引擎对SQL的解析、优化和物理执行有着非常明确的“偏好”。
Hive本质上是一个把SQL翻译成分布式计算任务的工具。它底层依赖的计算框架(比如MapReduce、Tez或Spark)决定了任务怎么切分、怎么并行、怎么落盘。很多人写Hive SQL的习惯是从关系型数据库带过来的,觉得“逻辑对就行”,但在分布式环境下,数据倾斜、小文件过多、不合理的Join顺序、全表扫描这些问题会被放大几十倍甚至上百倍。
这篇文章面向的是已经能写Hive SQL、但经常被性能问题卡住的开发者和数据工程师。我不会只丢几个参数让你去调,而是把每个优化动作背后的“为什么”讲清楚——为什么这个参数能生效,为什么那种写法会触发额外的Stage,为什么有时候加资源反而更慢。只有理解了执行逻辑,你才能在面对新问题时自己推导出优化方案,而不是背几条“祖传配置”。
接下来的内容会从执行计划解读、数据倾斜处理、Join策略选择、文件与分区管理、资源调优这几个核心维度展开,每个部分都会配上可复现的SQL示例和参数说明。你可以直接拿去对照自己的慢查询,也可以把这套思路当成排查性能问题的通用框架。
2. 先看懂执行计划,再谈优化
2.1 EXPLAIN到底在告诉你什么
很多人优化Hive的第一步是“凭感觉改SQL”,但真正有效的做法是先看执行计划。在Hive中,EXPLAIN语句会输出SQL被解析后的抽象语法树、逻辑计划、物理计划以及最终的执行阶段划分。你不需要看懂每一个字段,但有几个关键信息必须抓住。
执行计划里最核心的是Stage的划分。每个Stage代表一轮分布式计算,Stage越多,意味着中间落盘和网络传输的次数越多。一个常见的性能杀手就是“每个Stage都写一次HDFS”,因为HDFS的写入开销远大于内存计算。如果你看到执行计划里有大量MapReduce或Tez的Stage串联,就要考虑能不能合并。
另一个关键信息是Operator Tree。它展示了数据从表扫描到最终输出的完整流转路径。比如你看到一个TableScan后面跟着Filter,再跟着Select,最后才到Join,说明过滤条件没有下推,大量无用数据被读进来参与Join。理想情况下,过滤应该尽可能靠近TableScan,让数据在进入Shuffle之前就被裁剪掉。
EXPLAIN SELECT a.user_id, b.order_amount FROM user_info a JOIN order_detail b ON a.user_id = b.user_id WHERE a.dt = '2024-01-01' AND b.dt = '2024-01-01';执行这个语句后,重点看predicate部分是否出现在TableScan节点上。如果过滤条件出现在Join之后的Filter节点,说明分区裁剪没有生效,你需要检查分区字段的写法是否规范。
2.2 从执行计划里揪出“隐藏的Shuffle”
Shuffle是分布式计算中最昂贵的操作之一。它意味着数据要按Key重新分发到不同节点,涉及网络传输、序列化、磁盘IO。执行计划里出现ReduceSinkOperator就代表发生了Shuffle。有些Shuffle是业务逻辑必需的,比如Join和Group By;但有些Shuffle是写法不当引入的,完全可以避免。
一个典型的例子是COUNT(DISTINCT)。很多人写SELECT COUNT(DISTINCT user_id) FROM table,执行计划里会出现一个ReduceSink,把所有user_id汇聚到一个Reducer去重。如果user_id基数很大,单个Reducer会成为瓶颈。更好的做法是用GROUP BY加子查询:
SELECT COUNT(*) FROM ( SELECT user_id FROM table GROUP BY user_id ) t;这样去重工作分散到多个Reducer,最后再汇总计数。虽然逻辑等价,但执行计划完全不同,后者能充分利用并行度。
还有一个容易被忽略的点是隐式类型转换。如果Join的关联字段一边是String一边是BigInt,Hive会插入额外的转换操作,甚至导致无法使用MapJoin。执行计划里会体现为额外的SelectOperator。所以建表时字段类型要统一,Join时显式转换也比隐式转换更可控。
2.3 用EXPLAIN EXTENDED看更细的统计信息
普通EXPLAIN只展示逻辑和物理计划,EXPLAIN EXTENDED会额外输出表统计信息、列统计信息以及每个Operator的预估行数。这些统计信息是CBO(Cost-Based Optimizer)做决策的依据。如果统计信息缺失或过期,CBO可能选错Join策略或并行度。
比如你看到某个TableScan的numRows是-1或者一个明显不合理的值,说明表的统计信息没有收集。这时候需要执行ANALYZE TABLE ... COMPUTE STATISTICS来更新。统计信息准确后,CBO才能正确判断哪张表是小表、能不能走MapJoin、Shuffle的并行度设多少合适。
提示:
EXPLAIN EXTENDED的输出非常长,建议重定向到文件后用关键词搜索,重点关注Statistics和numRows字段。
3. 数据倾斜:那个让90%的Reducer空转的元凶
3.1 怎么判断自己遇到了数据倾斜
数据倾斜的典型表现是:任务整体进度卡在99%很久,打开任务监控发现大部分Reducer早就完成了,但有一两个Reducer处理的数据量是其他的几十倍甚至上百倍。这时候去看Shuffle阶段的记录数分布,会看到某个Key对应的记录数异常突出。
数据倾斜的根因通常是Join Key或Group By Key的分布不均匀。比如用户表里某个测试账号关联了海量订单,或者日志表里某个爬虫IP的请求量占了总量的一半。这些“热点Key”会把所有相关数据拉到同一个Reducer,导致单点过载。
判断倾斜不能只看任务时长,还要看Shuffle数据量的分布。在YARN或Tez的UI里,可以查看每个Reducer的输入记录数和字节数。如果最大值和平均值差距超过一个数量级,基本可以确认倾斜。
3.2 空值引发的倾斜及处理
空值倾斜是最常见也最容易修复的一类。当Join的关联字段存在大量NULL时,这些NULL会被Shuffle到同一个Reducer。如果两张表都有大量NULL,这个Reducer要处理的是笛卡尔积级别的数据量。
处理方式有两种。第一种是在Join条件里直接过滤掉NULL:
SELECT a.*, b.* FROM table_a a JOIN table_b b ON a.key = b.key WHERE a.key IS NOT NULL AND b.key IS NOT NULL;第二种是给NULL赋一个随机值,让它们分散到不同Reducer:
SELECT a.*, b.* FROM ( SELECT *, CASE WHEN key IS NULL THEN CONCAT('null_', RAND()) ELSE key END AS join_key FROM table_a ) a JOIN table_b b ON a.join_key = b.key;第二种方法适合业务上确实需要保留NULL记录的场景,但要注意随机值不能和真实Key冲突。
3.3 热点Key的拆分与打散
如果倾斜是由真实的热点Key引起的,处理起来更复杂一些。核心思路是把热点Key拆成多个子Key,分别计算后再合并。比如某个大客户ID关联了80%的订单,可以给这个ID加上随机后缀:
-- 第一步:给大表的热点Key打散 SELECT CONCAT(user_id, '_', CAST(FLOOR(RAND() * 10) AS INT)) AS user_id_split, order_amount FROM order_detail WHERE dt = '2024-01-01'; -- 第二步:小表膨胀对应倍数 SELECT CONCAT(user_id, '_', CAST(n AS INT)) AS user_id_split, user_name FROM user_info LATERAL VIEW EXPLODE(ARRAY(0,1,2,3,4,5,6,7,8,9)) t AS n WHERE dt = '2024-01-01';这样原本一个Reducer处理的数据被分散到10个Reducer,最后再对结果做一次聚合。代价是小表被放大了10倍,所以只适合小表足够小的场景。
注意:打散倍数不是越大越好。倍数太大反而会增加Shuffle数据量和最终合并的开销。一般从5到10开始试,观察Reducer负载是否均衡。
3.4 Group By倾斜的应对策略
Group By引起的倾斜和Join类似,但处理手段略有不同。除了打散Key之外,还可以启用Hive的负载均衡配置:
SET hive.groupby.skewindata = true;这个参数开启后,Hive会先做一次随机分发做部分聚合,再做一次最终聚合。相当于把一次Group By拆成两轮,第一轮让数据均匀分布,第二轮完成最终计算。代价是多一轮Shuffle,但能有效缓解倾斜。
另一个思路是分桶表。如果Group By的字段正好是分桶字段,Hive可以按桶并行处理,天然避免单点过载。建表时用CLUSTERED BY (key) INTO N BUCKETS,写入时确保数据按桶分布。
4. Join策略选对了,性能差十倍
4.1 MapJoin:把小表塞进内存
MapJoin是Hive中最有效的Join优化手段之一。它的原理是在Map阶段把小表加载到每个Map任务的内存中,大表数据流式读取时直接和内存中的小表做匹配,完全避免了Shuffle和Reduce阶段。
触发MapJoin的条件是小表的大小不超过配置阈值。默认阈值是25MB,可以通过参数调整:
SET hive.auto.convert.join = true; SET hive.mapjoin.smalltable.filesize = 25000000;但要注意,MapJoin不是万能的。如果小表实际大小远超阈值,或者Map任务的内存不够,会导致OOM。所以调整阈值前要先确认小表的真实大小,可以用DESCRIBE FORMATTED查看表的统计信息,或者直接dfs -du -h看HDFS上的文件大小。
还有一个坑是多个MapJoin串联。如果一条SQL里有多个MapJoin,Hive会尝试把它们合并成一个Map任务,这时候所有小表都要同时驻留内存。如果每个小表都接近阈值,合并后可能超出内存限制。可以通过hive.auto.convert.join.noconditionaltask.size控制合并后的大小上限。
4.2 Common Join的适用场景与调优
当两张表都很大,无法用MapJoin时,Hive会走Common Join(也叫Reduce Join)。它的流程是:Map阶段读取两张表,按Join Key做Shuffle,Reduce阶段完成匹配。这个过程的性能瓶颈通常在Shuffle和Reduce端的排序。
优化Common Join的核心是减少参与Shuffle的数据量。具体做法包括:在Map阶段尽早过滤数据、只Select需要的列、避免在Join字段上做函数转换。比如下面这种写法就会导致无法使用索引和分区裁剪:
-- 不推荐:Join字段被函数包裹 SELECT * FROM a JOIN b ON UPPER(a.code) = UPPER(b.code); -- 推荐:提前处理好字段 SELECT * FROM ( SELECT UPPER(code) AS code, ... FROM a ) a1 JOIN ( SELECT UPPER(code) AS code, ... FROM b ) b1 ON a1.code = b1.code;另外,Join的顺序也会影响性能。在多表Join时,Hive默认按照SQL中写的顺序依次Join。如果先Join产生的结果集很大,后续Join的Shuffle压力就会很大。更好的做法是把过滤后数据量最小的表放在前面,或者用子查询先做聚合再Join。
4.3 Bucket MapJoin:分桶表的Join加速
如果两张表都按Join Key分了桶,并且桶的数量成倍数关系,Hive可以使用Bucket MapJoin。它的原理是:每个Map任务只读取对应桶的数据,在Map端完成Join,不需要Reduce阶段。
启用Bucket MapJoin需要满足几个条件:两张表都是分桶表、Join Key是分桶字段、小表的桶数是大表的整数倍。配置参数如下:
SET hive.optimize.bucketmapjoin = true; SET hive.optimize.bucketmapjoin.sortedmerge = true;Bucket MapJoin的优势是数据局部性极好,每个Map任务处理的数据量可控。但它对建表要求高,而且一旦数据写入时没有严格按桶分布,Join结果就会出错。所以使用前要确认分桶表的写入过程是可靠的。
4.4 SMB Join:排序合并分桶Join
SMB Join(Sort-Merge-Bucket Join)是Bucket MapJoin的升级版。它要求两张表不仅分桶,而且桶内数据按Join Key排序。这样Join时可以用归并排序的方式做匹配,效率更高。
SET hive.input.format = org.apache.hadoop.hive.ql.io.BucketizedHiveInputFormat; SET hive.optimize.bucketmapjoin = true; SET hive.optimize.bucketmapjoin.sortedmerge = true;SMB Join适合大表对大表的场景,但建表和维护成本也最高。如果业务对Join性能要求极高,且Join Key相对固定,可以考虑这种方案。
5. 文件、分区与存储格式的隐形影响
5.1 小文件问题:NameNode的噩梦
Hive表底层是HDFS文件,每个文件在NameNode里都要占一条元数据。如果表里有大量小文件(比如每个几KB),不仅NameNode压力大,查询时每个小文件都要启动一个Map任务,任务启动和调度的开销远超数据处理本身。
小文件通常来自几个场景:频繁的INSERT操作、动态分区插入、Reduce数量过多。解决小文件问题可以从写入和读取两端入手。
写入端控制Reduce数量:
SET hive.exec.reducers.bytes.per.reducer = 256000000; SET hive.exec.reducers.max = 1009;读取端合并小文件:
SET hive.input.format = org.apache.hadoop.hive.ql.io.CombineHiveInputFormat; SET mapreduce.input.fileinputformat.split.maxsize = 256000000;CombineHiveInputFormat会把多个小文件合并到一个Map任务里处理,减少任务数量。split.maxsize控制每个Map处理的最大数据量,避免单个Map过大。
对于已经存在大量小文件的表,可以用INSERT OVERWRITE重写一遍,或者用Hive的CONCATENATE命令合并:
ALTER TABLE table_name PARTITION (dt='2024-01-01') CONCATENATE;注意:
CONCATENATE只对TextFile格式有效,ORC和Parquet有自己的合并机制。
5.2 分区裁剪:别让全表扫描拖垮查询
分区是Hive最基本的裁剪手段。如果表按日期分区,查询时带上WHERE dt = '2024-01-01',Hive就只会扫描对应分区目录。但分区裁剪生效的前提是分区字段不能参与函数运算。
-- 分区裁剪生效 SELECT * FROM logs WHERE dt = '2024-01-01'; -- 分区裁剪失效,变成全表扫描 SELECT * FROM logs WHERE SUBSTR(dt, 1, 4) = '2024';如果业务上确实需要按月份查询,更好的做法是建两级分区(年、月),或者用WHERE dt BETWEEN '2024-01-01' AND '2024-01-31'这种范围条件。
动态分区插入时也要注意,如果分区字段基数太大,会生成海量分区和小文件。可以通过hive.exec.max.dynamic.partitions限制最大分区数,避免一次插入产生过多分区。
5.3 ORC与Parquet:列式存储的威力
行式存储(TextFile、SequenceFile)在分析场景下效率很低,因为查询通常只涉及少数几列,但行存会把整行数据都读进来。列式存储(ORC、Parquet)按列组织数据,查询时只读取涉及的列,IO量能减少一个数量级。
ORC格式还支持谓词下推和布隆过滤器。谓词下推让过滤条件在存储层就生效,不需要读出不匹配的数据。布隆过滤器可以快速判断某个Key是否在文件中,加速Join和过滤。
建表时指定ORC格式:
CREATE TABLE user_behavior ( user_id BIGINT, item_id BIGINT, category STRING, behavior STRING, ts BIGINT ) STORED AS ORC TBLPROPERTIES ('orc.compress' = 'SNAPPY');ORC的压缩块大小和索引粒度也可以调优。orc.compress推荐用SNAPPY,压缩比和CPU开销比较均衡。orc.stripe.size控制每个Stripe的大小,默认64MB,大表可以适当调大。
5.4 存储格式转换的实操步骤
如果现有表是TextFile格式,想转成ORC,不能直接ALTER TABLE改格式,需要新建表再导入:
-- 1. 创建ORC格式的新表 CREATE TABLE user_behavior_orc LIKE user_behavior STORED AS ORC; -- 2. 导入数据 INSERT OVERWRITE TABLE user_behavior_orc SELECT * FROM user_behavior; -- 3. 验证数据量和抽样 SELECT COUNT(*) FROM user_behavior_orc; SELECT * FROM user_behavior_orc LIMIT 10;转换过程中要注意数据类型兼容性。TextFile里的NULL和空字符串在ORC里可能有不同表现,转换后要抽样验证。另外,ORC对复杂类型(Array、Map、Struct)的支持和TextFile略有差异,转换前最好先在小分区上测试。
6. 资源与并行度:不是加内存就能快
6.1 Map和Reduce数量的合理区间
Map数量由输入分片决定,通常不需要手动干预。但如果输入有大量小文件,Map数量会爆炸。前面提到的CombineHiveInputFormat可以缓解这个问题。Reduce数量则更可控,默认由hive.exec.reducers.bytes.per.reducer和hive.exec.reducers.max共同决定。
Reduce数量太少,单个Reducer处理的数据量大,容易成为瓶颈;Reduce数量太多,每个Reducer处理的数据量小,但任务启动和调度的开销占比高。经验值是每个Reduce处理128MB到256MB数据。可以通过执行计划里的numReduceTasks预估,也可以根据Shuffle数据总量除以目标值来手动设置:
SET mapreduce.job.reduces = 200;但手动设置Reduce数量要谨慎,因为不同Stage的数据量可能差异很大。更好的做法是让Hive自动计算,只在确认自动值不合理时才干预。
6.2 内存配置的常见误区
很多人遇到OOM的第一反应是加内存,但加内存不一定能解决问题。如果是数据倾斜导致的单Reducer OOM,加内存只是延缓了崩溃时间,根本解法还是打散Key。如果是MapJoin的小表太大导致OOM,加内存可以缓解,但更稳妥的做法是调低MapJoin阈值或改用Common Join。
Container内存的配置涉及几个参数:
SET mapreduce.map.memory.mb = 4096; SET mapreduce.reduce.memory.mb = 8192; SET mapreduce.map.java.opts = -Xmx3276m; SET mapreduce.reduce.java.opts = -Xmx6553m;注意JVM堆内存(java.opts)要比Container内存小一些,留出空间给JVM本身和堆外内存。一般堆内存设为Container的80%左右。
6.3 并行执行与推测执行
Hive的Stage之间默认是串行的,前一个Stage完成才能开始下一个。如果多个Stage之间没有依赖关系,可以开启并行执行:
SET hive.exec.parallel = true; SET hive.exec.parallel.thread.number = 8;这能显著缩短整体任务时间,但会占用更多资源。如果集群资源紧张,并行执行反而会导致任务排队。
推测执行是另一个双刃剑。它会在某个任务跑得特别慢时启动一个备份任务,谁先完成用谁的结果。对于数据倾斜导致的长尾任务,推测执行能加速;但对于资源紧张的集群,备份任务会挤占其他任务的资源。建议在确认没有数据倾斜的情况下关闭推测执行:
SET mapreduce.map.speculative = false; SET mapreduce.reduce.speculative = false;6.4 JVM重用与任务调度开销
每个Map或Reduce任务默认启动一个新的JVM,JVM启动和销毁的开销在任务数量多时非常可观。开启JVM重用可以让一个JVM连续执行多个任务:
SET mapreduce.job.jvm.numtasks = 10;但JVM重用也有代价:如果某个任务有内存泄漏,会影响后续任务。所以一般设置一个适中的值(比如10),不要设得太大。
另外,任务调度器的选择也会影响性能。Capacity Scheduler适合多租户共享集群,FIFO Scheduler适合独占集群。如果集群资源充足但任务排队严重,可以检查调度器的队列配置和资源池划分。
7. 那些年我踩过的Hive性能坑
7.1 COUNT(DISTINCT)的替代方案实测
前面提到用GROUP BY替代COUNT(DISTINCT),但实际测试中还要注意数据分布。如果去重后的基数很小(比如几万),COUNT(DISTINCT)可能更快,因为它的Reducer压力不大。如果基数很大(几百万以上),GROUP BY的优势才明显。
还有一种方案是用APPROX_COUNT_DISTINCT,它基于HyperLogLog算法,用少量内存估算基数,误差在1%到2%左右。对于不需要精确值的场景(比如UV统计),这个函数能大幅减少计算量:
SELECT APPROX_COUNT_DISTINCT(user_id) FROM user_behavior;实测下来,在基数千万级的表上,APPROX_COUNT_DISTINCT比精确去重快5到10倍,内存占用也低得多。
7.2 动态分区插入的坑与修复
动态分区插入是ETL中的常用操作,但很容易踩坑。最常见的问题是分区数过多导致NameNode压力和小文件爆炸。比如按用户ID分区,几百万用户就是几百万个分区,每个分区一个小文件。
修复方案是限制动态分区的数量,并在插入后合并小文件:
SET hive.exec.dynamic.partition = true; SET hive.exec.dynamic.partition.mode = nonstrict; SET hive.exec.max.dynamic.partitions = 10000; SET hive.exec.max.dynamic.partitions.pernode = 1000;如果业务上确实需要按高基数字段分区,建议改用分桶表,或者把高基数字段作为普通列,用分区加排序的方式组织数据。
7.3 数据倾斜排查的完整链路
遇到任务卡在99%时,我的排查顺序是这样的:
- 看任务监控:确认是哪个Stage卡住,是Map还是Reduce。
- 看Shuffle分布:在YARN或Tez UI里查看每个Reducer的输入记录数,找出异常值。
- 定位热点Key:对Shuffle字段做
GROUP BY加COUNT,按数量降序排列,找出Top 10的Key。 - 分析Key的业务含义:判断是空值、测试数据还是真实热点。
- 选择处理方案:空值过滤或打散,热点Key拆分或加随机后缀。
- 验证效果:重新跑任务,观察Reducer负载是否均衡。
这个链路看起来简单,但每一步都需要对数据和业务有理解。比如定位到热点Key后,不能盲目打散,要先确认这个Key在业务上是否重要、打散后会不会影响结果准确性。
7.4 参数调优的边界与验证方法
Hive的参数有几百个,但真正影响性能的也就二三十个。调参的原则是一次只改一个参数,改完必须验证。我见过有人一次性改了十几个参数,结果任务变快了,但不知道是哪个参数起了作用,下次遇到类似问题还是不会调。
验证方法也很重要。不能只看任务总时长,还要看每个Stage的耗时变化、Shuffle数据量变化、CPU和内存使用率变化。有时候总时长没变,但某个Stage的耗时减少了,说明优化方向是对的,只是被其他Stage的瓶颈掩盖了。
另外,参数调优要考虑集群的整体负载。你把自己的任务调快了,可能占用了更多资源,导致别人的任务变慢。在共享集群里,公平性和效率需要平衡。
7.5 一个真实案例的完整复盘
曾经遇到一个每天跑3小时的ETL任务,逻辑是两张亿级表Join后聚合。执行计划显示走了Common Join,Shuffle数据量约2TB。排查后发现Join Key里有大量NULL,且其中一张表有严重的热点Key。
处理过程分三步:第一步过滤NULL,Shuffle数据量降到1.2TB;第二步对热点Key加随机后缀打散,Reducer负载从最大80GB降到平均8GB;第三步把结果表改成ORC格式并开启向量化查询。最终任务时间从3小时降到25分钟。
这个案例说明,性能优化往往不是单一手段能解决的,而是多个小优化的叠加。每一步可能只提升20%到30%,但组合起来就是数量级的差异。
8. 向量化与CBO:让Hive自己变聪明
8.1 向量化查询的执行原理
向量化查询是Hive 0.13引入的重要特性,它一次处理1024行数据,而不是逐行处理。这能大幅减少CPU指令分支和函数调用开销,在扫描和过滤场景下提升非常明显。
开启向量化:
SET hive.vectorized.execution.enabled = true; SET hive.vectorized.execution.reduce.enabled = true;但向量化对数据类型有要求。它支持大多数基本类型,但对复杂类型(如Array、Map)的支持有限。如果查询里大量使用复杂类型函数,向量化可能不生效。可以通过执行计划里的Vectorized标记确认是否启用。
另外,向量化对ORC格式的支持最好,Parquet次之,TextFile基本没有效果。所以向量化要和列式存储配合使用。
8.2 CBO的统计信息收集与维护
CBO(Cost-Based Optimizer)是Hive 0.14引入的基于代价的优化器。它会根据表的统计信息(行数、列基数、数据大小)选择Join顺序、Join策略和并行度。如果统计信息缺失,CBO就退化成基于规则的优化器,效果大打折扣。
收集统计信息:
ANALYZE TABLE table_name COMPUTE STATISTICS; ANALYZE TABLE table_name COMPUTE STATISTICS FOR COLUMNS col1, col2;前者收集表级统计信息(行数、文件数、大小),后者收集列级统计信息(基数、NULL值数量、平均长度)。列级统计信息对CBO更重要,但收集开销也更大。建议对经常参与Join和Group By的列定期收集。
统计信息不是一劳永逸的。数据每天新增后,统计信息就过期了。可以配置Hive的自动统计收集:
SET hive.stats.autogather = true; SET hive.stats.column.autogather = true;自动收集在INSERT时触发,对动态分区插入也有效。但要注意,自动收集会增加写入时间,如果写入性能敏感,可以改为定时手动收集。
8.3 基于代价的Join顺序调整
CBO最直观的效果是自动调整Join顺序。在没有CBO时,Hive按SQL书写顺序Join;有了CBO,它会根据表的大小和过滤后的行数,把最小的结果集放在前面。
比如下面这个查询:
SELECT * FROM large_table a JOIN medium_table b ON a.id = b.id JOIN small_table c ON a.id = c.id;CBO可能会先Joinsmall_table和medium_table,再和large_tableJoin,因为前两者Join后结果集更小。这个调整对性能影响很大,尤其是多表Join的场景。
但CBO的决策依赖统计信息的准确性。如果统计信息过期,CBO可能做出错误选择。所以定期收集统计信息是使用CBO的前提。
8.4 向量化与CBO的配合使用
向量化和CBO不是互斥的,而是互补的。CBO决定“怎么执行”,向量化决定“执行得多快”。两者同时开启,效果最好。
但要注意,向量化对某些CBO选择的执行计划可能不支持。比如CBO选择了Bucket MapJoin,但向量化在Bucket MapJoin上的支持有限。这时候需要权衡:是关掉向量化用Bucket MapJoin,还是关掉Bucket MapJoin用向量化。通常建议先测试两种组合的实际耗时,再决定。
我在实际使用中的体会是,对于简单的扫描过滤聚合查询,向量化提升明显;对于复杂的多表Join,CBO的Join顺序调整更重要。两者都开启后,再根据执行计划微调参数,基本能覆盖大部分场景。