hive的explain执行计划查看
HIVE 的 EXPLAIN 执行计划查看
HIVE SQL 从提交到执行过程
- 从用户界面 (UI) 到驱动器 (Driver):查询首先通过Hive命令行界面 (CLI)、Beeline或JDBC/ODBC等客户端提交 。驱动器接收到查询请求后,会创建一个会话来管理该查询的生命周期 。
- 从驱动器到编译器 (Compiler):驱动器将查询字符串传递给编译器,请求其生成一个执行计划 。
- 编译与优化:这是整个过程的核心。
- 编译器首先与元数据存储 (Metastore) 通信,获取查询中涉及的表、分区、列和数据类型等元数据信息。这些信息对于后续的语义分析和类型检查至关重要 。
- 接着,查询被解析成一个抽象语法树 (Abstract Syntax Tree, AST),这是一种树状的数据结构,精确地表示了查询的语法结构 。
- AST随后被转换为逻辑计划,这是一个由逻辑操作符组成的图。此时,查询优化器(例如基于Apache Calcite的成本优化器,Cost-Based Optimizer, CBO)会介入,应用一系列优化规则(如谓词下推、列裁剪等)对逻辑计划进行重写和优化,最终生成一个物理执行计划的有向无环图 (Directed Acyclic Graph, DAG) 。
- 从计划到执行引擎 (Execution Engine):经过优化的物理计划,也就是我们所说的“执行计划”,由驱动器发送给选定的执行引擎,如MapReduce、Tez或Spark 。
- 执行:执行引擎接收到计划后,将其翻译成具体的物理任务(如MapReduce作业、Tez DAG中的顶点任务或Spark作业中的任务),并通过YARN在Hadoop集群上调度和执行这些任务 。
使用explain查看生成的执行计划,即拦截第5步之前的物理计划,后续步骤不执行,返回给我用户可读的执行计划,查看执行计划可以了解sql的存在的问题,和一些需要可以优化的改进点。
EXPLAIN 语法
sqlEXPLAIN [EXTENDED|CBO|AST|DEPENDENCY|AUTHORIZATION|LOCKS|VECTORIZATION|ANALYZE] query
- CBO hive有两个版本的优化器,一个是基于规则定义的的优化,一个是基于计算成本的优化(该优化依赖于数据的完整的元数据信息),现在普遍使用的是基于成本的优化,即使CBO
- AST 查看ast语法树
- DEPENDENCY 查看所需要数据的元数据,比如表名,表分区,基于这个特定,可以现实一些简单的数据仓库血缘管理的功能
- AUTHORIZATION 查看此处查询所需要的授权信息,比如hdfs的文件路径,查询的用户名,输入的表
- VECTORIZATION 查看此次查询各个节点map和reduce所使用的向量化信息,比如查询所使用的运算符是否支持向量化,字段是否支持向量化等
- ANALYZE 展示查询计算预估的数据量和实际执行时的数据量
- LOCKS 展示执行查询时系统需要锁定的内容
- EXTENDED 主要可以查看抽象语法树结构,作业依赖关系,每个作业的详情
EXPLAIN基本结构
以下测试都基于 hive on spark
sqlexplain select city, count(1) numbers from measurement_lite where city_hash = 938117018 and measurement > 30 group by city
查询结果:
主要两个部分 STAGE DEPENDENCIES 和 STAGE PLANS,
STAGE DEPENDENCIES表示各个阶段的依赖关系,Stage-1为根阶段,Stage-0依赖于Stage-1,只有 Stage-1执行完毕,Stage-0才会执行,需要注意 stage-序号,其中序号的大小并不表示依赖的先后顺序。
bashSTAGE DEPENDENCIES: Stage-1 is a root stage Stage-0 depends on stages: Stage-1
STAGE PLANS表示各个阶段的执行详情。
Stage-1
sqlMap 1 Map Operator Tree: TableScan #进行表扫描 alias: measurement_lite # 需要扫描的表别名为measurement_lite Statistics: Num rows: 2421668 Data size: 494020272 Basic stats: COMPLETE Column stats: NONE # 数据基本情况,行数,数据大小,基本数据情况是否完整,以及列的一些信息 Filter Operator # 数据过滤 predicate: (measurement > 30) (type: boolean) # 过滤条件 Statistics: Num rows: 807222 Data size: 164673288 Basic stats: COMPLETE Column stats: NONE ## 过滤后的数据情况 Select Operator # 列裁剪,筛选后续需要的字段,此处需要city expressions: city (type: string) # 字段类型 outputColumnNames: city # 字段别名 Statistics: Num rows: 807222 Data size: 164673288 Basic stats: COMPLETE Column stats: NONE # 列裁剪后的数据情况 Group By Operator # map端的分组聚合操作,也称为map端预聚合或者combiner aggregations: count() # 聚合方式 keys: city (type: string) # 按照哪些key进行分组聚合 mode: hash outputColumnNames: _col0, _col1 # 输出列名字 Statistics: Num rows: 807222 Data size: 164673288 Basic stats: COMPLETE Column stats: NONE # 数据情况 Reduce Output Operator # 发往reduce时的处理,即map的输出(为key-value结构) key expressions: _col0 (type: string) # key计算表达式,可能为多个列计算得到 sort order: + # 多个列的排序方式,此处只有一个列,且为升序;如果为 +- 表示第一个列升序,第二个列降序 Map-reduce partition columns: _col0 (type: string) # 表示Map阶段输出到Reduce阶段的分区列,在HiveSQL中,可以用distribute by指定分区的列 Statistics: Num rows: 807222 Data size: 164673288 Basic stats: COMPLETE Column stats: NONE value expressions: _col1 (type: bigint) # value值字段 Execution mode: vectorized # 表示可以向量化处理
一般的查询操作一次只处理一行数据,在向量化查询执行时一次处理1024行的块来简化系统底层的操作,提高了数据处理的性能
hive向量化模式使用前置条件
- 不同的计算引擎支持程度不一样:MR计算引擎仅支持Map阶段的向量化,Tez和Spark计算引擎可以支持Map阶段和Reduce阶段的向量化。
- hive文件存储类型必须为ORC或者Parquet等列存储文件类型。
sqlReducer 2 Execution mode: vectorized # 向量化处理 Reduce Operator Tree: Group By Operator # 分组聚合操作 aggregations: count(VALUE._col0) # 聚合逻辑 keys: KEY._col0 (type: string) # 聚合作用的列 mode: mergepartial # 聚合模式,值有 hash:随机聚合;mergepartial:合并部分聚合结果;final:最终聚合 outputColumnNames: _col0, _col1 # 聚合输出列 Statistics: Num rows: 403611 Data size: 82336644 Basic stats: COMPLETE Column stats: NONE # 数据量,可以看到通过map端预聚合,到达reducer后数据量减少了 File Output Operator # 文件输出操作 compressed: false # 是否压缩 Statistics: Num rows: 403611 Data size: 82336644 Basic stats: COMPLETE Column stats: NONE table: input format: org.apache.hadoop.mapred.SequenceFileInputFormat # 输入文件类型 output format: org.apache.hadoop.hive.ql.io.HiveSequenceFileOutputFormat # 输出文件类型 serde: org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe # 读取表数据的序列化和反序列化方式。
Stage-0:
sqlStage: Stage-0 # 阶段Stage-0 Fetch Operator # 客户端获取数据操作 limit: -1 # 获取数据量 -1表示全部 Processor Tree: # 处理器树 ListSink # 数据展示
总结 hivesql执行过程:
使用 EXPLAIN DEPENDENCY甄别谓词下推是否生效的不同场景
什么是谓词下推
通过将过滤条件提前作用于需要查询的数据集达到减少需要处理的数据量,从而提高查询效率的手段(predicate push down)
能否谓词下推取决于谓词的表达式,数据表原本的存储格式(非列式存储不支持)
支持下推的表达式
- 简单比较:
=,>,<,>=,<=,<>,!= IN/NOT INIS NULL/IS NOT NULLBETWEEN ... AND ...LIKE(部分情况,取决于存储格式)- 逻辑运算符:
AND,OR(需拆分为多个子谓词)
不支持下推的表达式
- UDF(用户自定义函数)
- 非确定性函数:
RAND(),UUID(),CURRENT_TIMESTAMP() - 复杂表达式:
SUBSTR(a,1,2) = 'ab',除非是列裁剪优化 - 子查询中的谓词:
WHERE id IN (SELECT ... FROM ...)
支持谓词下推的存储格式
- ORC / Parquet:这两种列式存储格式天然支持谓词下推,因为它们在写入时会生成统计信息(如 min/max 值、null 计数等)和索引(如 ORC 的 row index、BloomFilter)。
- Avro:部分支持,但不如 ORC/Parquet 高效。
- TextFile / SequenceFile:不支持谓词下推,因为它们没有内置的统计信息或索引机制。
使用 explain dependency 检查是否谓词是否下推
测试表情况:
sql+----------------------------------------------------------------------+ |createtab_stmt | +----------------------------------------------------------------------+ |CREATE TABLE `mydb.measurement_lite`( | | `city` string, | | `measurement` decimal(10,2)) | |PARTITIONED BY ( | | `city_hash` int) | |ROW FORMAT SERDE | | 'org.apache.hadoop.hive.ql.io.orc.OrcSerde' | |STORED AS INPUTFORMAT | | 'org.apache.hadoop.hive.ql.io.orc.OrcInputFormat' | |OUTPUTFORMAT | | 'org.apache.hadoop.hive.ql.io.orc.OrcOutputFormat' | |LOCATION | | 'hdfs://hadoop102:8020/user/hive/warehouse/mydb.db/measurement_lite'| |TBLPROPERTIES ( | | 'orc.compress'='ZLIB', | | 'transient_lastDdlTime'='1756196960') | +----------------------------------------------------------------------+ +---------------------------------------------------------------+ |createtab_stmt | +---------------------------------------------------------------+ |CREATE TABLE `mydb.city_info`( | | `city_id` int COMMENT '城市ID', | | `city_name` string COMMENT '城市名称', | | `country` string COMMENT '所属国家', | | `province` string COMMENT '省份/州', | | `population` bigint COMMENT '人口数量', | | `longitude` decimal(10,6) COMMENT '经度', | | `latitude` decimal(10,6) COMMENT '纬度', | | `post_code` string COMMENT '邮政编码', | | `area_code` string COMMENT '区号', | | `created_at` timestamp COMMENT '创建时间') | |PARTITIONED BY ( | | `city_hash` int COMMENT '?????') | |ROW FORMAT SERDE | | 'org.apache.hadoop.hive.ql.io.orc.OrcSerde' | |WITH SERDEPROPERTIES ( | | 'field.delim'=',', | | 'serialization.format'=',') | |STORED AS INPUTFORMAT | | 'org.apache.hadoop.hive.ql.io.orc.OrcInputFormat' | |OUTPUTFORMAT | | 'org.apache.hadoop.hive.ql.io.orc.OrcOutputFormat' | |LOCATION | | 'hdfs://hadoop102:8020/user/hive/warehouse/mydb.db/city_info'| |TBLPROPERTIES ( | | 'bucketing_version'='2', | | 'transient_lastDdlTime'='1756197746') | +---------------------------------------------------------------+
两张表都具有 990497647, 981918035,951221382,938117018,90761778这5个分区,在 a left join b的语句中称a为基表,b为从表
sql-- 如下2端sql所表达的含义是不同的 -- 方式1 explain dependency SELECT a.measurement FROM mydb.measurement_lite a LEFT OUTER JOIN city_info b ON a.city_hash = b.city_hash AND a.city_hash BETWEEN 938117018 AND 990497647; -- 方式2 explain dependency SELECT a.measurement FROM mydb.measurement_lite a LEFT OUTER JOIN city_info b ON a.city_hash = b.city_hash WHERE a.city_hash BETWEEN 938117018 AND 990497647;
方式1查询结果:发现2张表都没有进行数据的筛减,即不满足谓词下推条件
json{ "input_tables": [ { "tablename": "mydb@measurement_lite", "tabletype": "MANAGED_TABLE" }, { "tablename": "mydb@city_info", "tabletype": "MANAGED_TABLE" } ], "input_partitions": [ { "partitionName": "mydb@measurement_lite@city_hash=90761778" }, { "partitionName": "mydb@measurement_lite@city_hash=938117018" }, { "partitionName": "mydb@measurement_lite@city_hash=951221382" }, { "partitionName": "mydb@measurement_lite@city_hash=981918035" }, { "partitionName": "mydb@measurement_lite@city_hash=990497647" }, { "partitionName": "mydb@city_info@city_hash=90761778" }, { "partitionName": "mydb@city_info@city_hash=938117018" }, { "partitionName": "mydb@city_info@city_hash=951221382" }, { "partitionName": "mydb@city_info@city_hash=981918035" }, { "partitionName": "mydb@city_info@city_hash=990497647" } ] }
方式2查询结果:发现2张表对都各个表数据进行了筛选
json{ "input_tables": [ { "tablename": "mydb@measurement_lite", "tabletype": "MANAGED_TABLE" }, { "tablename": "mydb@city_info", "tabletype": "MANAGED_TABLE" } ], "input_partitions": [ { "partitionName": "mydb@measurement_lite@city_hash=938117018" }, { "partitionName": "mydb@measurement_lite@city_hash=951221382" }, { "partitionName": "mydb@measurement_lite@city_hash=981918035" }, { "partitionName": "mydb@measurement_lite@city_hash=990497647" }, { "partitionName": "mydb@city_info@city_hash=938117018" }, { "partitionName": "mydb@city_info@city_hash=951221382" }, { "partitionName": "mydb@city_info@city_hash=981918035" }, { "partitionName": "mydb@city_info@city_hash=990497647" } ] }
sql-- 如下两段sql表达含义一致 -- 方式a explain dependency SELECT a.measurement FROM mydb.measurement_lite a LEFT OUTER JOIN city_info b ON a.city_hash = b.city_hash AND b.city_hash BETWEEN 938117018 AND 990497647; -- 方式b explain dependency SELECT a.measurement FROM mydb.measurement_lite a LEFT OUTER JOIN city_info b ON a.city_hash = b.city_hash WHERE b.city_hash BETWEEN 938117018 AND 990497647;
方式a输出:发现基表 measurement_lite 为全分区,从表city_info筛选了分区,谓词下推对从表生效了
json{ "input_tables": [ { "tablename": "mydb@measurement_lite", "tabletype": "MANAGED_TABLE" }, { "tablename": "mydb@city_info", "tabletype": "MANAGED_TABLE" } ], "input_partitions": [ { "partitionName": "mydb@measurement_lite@city_hash=90761778" }, { "partitionName": "mydb@measurement_lite@city_hash=938117018" }, { "partitionName": "mydb@measurement_lite@city_hash=951221382" }, { "partitionName": "mydb@measurement_lite@city_hash=981918035" }, { "partitionName": "mydb@measurement_lite@city_hash=990497647" }, { "partitionName": "mydb@city_info@city_hash=938117018" }, { "partitionName": "mydb@city_info@city_hash=951221382" }, { "partitionName": "mydb@city_info@city_hash=981918035" }, { "partitionName": "mydb@city_info@city_hash=990497647" } ] }
方式b输出:基表和从表都未进行数据的裁剪
json{ "input_tables": [ { "tablename": "mydb@measurement_lite", "tabletype": "MANAGED_TABLE" }, { "tablename": "mydb@city_info", "tabletype": "MANAGED_TABLE" } ], "input_partitions": [ { "partitionName": "mydb@measurement_lite@city_hash=90761778" }, { "partitionName": "mydb@measurement_lite@city_hash=938117018" }, { "partitionName": "mydb@measurement_lite@city_hash=951221382" }, { "partitionName": "mydb@measurement_lite@city_hash=981918035" }, { "partitionName": "mydb@measurement_lite@city_hash=990497647" }, { "partitionName": "mydb@city_info@city_hash=90761778" }, { "partitionName": "mydb@city_info@city_hash=938117018" }, { "partitionName": "mydb@city_info@city_hash=951221382" }, { "partitionName": "mydb@city_info@city_hash=981918035" }, { "partitionName": "mydb@city_info@city_hash=990497647" } ] }
综上:left outer join中针对右表非等值条件on和where查询数据左表都是全表扫描,右表on条件是条件过滤,where条件是全表扫描。
测试不同的join情况,得出如下结论:
因此对inner join 和 full outer join,过滤条件写在 on条件还是where条件都是相同的效果;full outer join无论过滤条件写在on还是where都不会进行谓词下推;只有left/right outer join需要分情况讨论。
使用 EXPAIN 查看设置的 MAP JOIN 是否生效
sqlexplain SELECT t1.province, COUNT(1) numbers FROM city_info t1 LEFT JOIN mydb.measurement_lite t2 ON t1.city_hash = t2.city_hash AND t2.measurement > 30 GROUP BY t1.province;
由于 measurement_lite表数据量很少,大概2.3GB,我想使用map join的方式。查看目前的执行计划:
大概的流程就是,分别读取 measurement_lite 和 city_info的数据,采用的PARTITION-LEVEL SORT(每个 Map 任务会先在内部对自己将要输出到各个分区的数据进行排序。也就是说,发往同一个 Reducer 的数据,在到达之前就已经是按照 Key(city_hash)排好序的)。其中 measurement_lite读取时谓词下推生效了,即过滤条件t2.measurement > 30;接下来进行第一次按照字段city_hash的shffule,在Reduce 2内执行 left outer join操作,其中也进行简单的count()聚合操作 Group By Operate;在Reduce 3 执行最终按照 privince_code的聚合;stage-0 执行数据的抓取。这个过程就是一个sort merge join的过程。整个过程耗时 4s左右。
想要改造为 map join的方式:
sqlset hive.auto.convert.join = true; set hive.mapjoin.smalltable.filesize = 900000000; -- 3. 【关键】开启更现代的 MapJoin 实现方式,避免独立的条件任务 set hive.auto.convert.join.noconditionaltask = true; -- 4. 【关键】这个参数与上面的 noconditionaltask 配合使用,指定大小检查的阈值 set hive.auto.convert.join.noconditionaltask.size = 900000000; explain SELECT /*+ MAPJOIN(t2) */ t1.province, COUNT(1) numbers FROM city_info t1 LEFT JOIN mydb.measurement_lite t2 ON t1.city_hash = t2.city_hash AND t2.measurement > 30 GROUP BY t1.province
计划解读: stage-2 为小表准备阶段,在内存中构建一个以 Join Key (city_hash) 为键的哈希表。这个哈希表随后会被分发(广播)到每一个即将处理大表数据的节点上
stage-1 Map 1为执行map join的主要过程,Reduce 2为最终结果的聚合。
stage-0 数据的抓取展示
整个查询过程大概 2s左右,可以看到执行上是有优化的。
map join生效的条件:
- 语法需要正确
- 小表大小需要符合参数的设定。这里的小表大小指的是通过谓词下推或者其他条件过滤后的表大小,比如此处
measurement_lite全部为2.3GB,通过谓词下推后大小有790MB左右
map join生效的执行过程特定:有一个阶段读取小表构建hash表,并将数据写入到hdfs上,并构建spark的分布式缓存,并接下来的阶段就是读取小表数据并进行map join
使用 EXPLAIN 验证 sort merge bucket map join 是否生效
分区是为了减少数据查询时扫描的范围,在hdfs上体现为具体的目录;而分桶的目的是为了使得存储具体数据的文件大小更加均匀,一定程度山避免数据倾斜。
需要的分桶表:
sqlCREATE TABLE city_info_bucketed ( city_id INT COMMENT '城市ID', city_name STRING COMMENT '城市名称', country STRING COMMENT '所属国家', province STRING COMMENT '省份/州', population BIGINT COMMENT '人口数量', longitude DECIMAL(10, 6) COMMENT '经度', latitude DECIMAL(10, 6) COMMENT '纬度', post_code STRING COMMENT '邮政编码', area_code STRING COMMENT '区号', created_at TIMESTAMP COMMENT '创建时间', city_hash int COMMENT '城市哈希值' ) CLUSTERED BY (city_hash) INTO 32 BUCKETS STORED AS ORC; CREATE TABLE measurement_lite_bucketed ( `city` string comment '城市名字', `measurement` decimal(10,2) comment '温度测量值', `city_hash` int COMMENT '城市哈希值' ) CLUSTERED BY (city_hash) INTO 32 BUCKETS STORED AS ORC;
sqlset hive.auto.convert.join = true; set hive.mapjoin.smalltable.filesize = 900000000; -- 3. 【关键】开启更现代的 MapJoin 实现方式,避免独立的条件任务 set hive.auto.convert.join.noconditionaltask = true; -- 4. 【关键】这个参数与上面的 noconditionaltask 配合使用,指定大小检查的阈值 set hive.auto.convert.join.noconditionaltask.size = 900000000; set hive.optimize.bucketmapjoin = true; EXPLAIN SELECT t1.province, COUNT(1) AS numbers FROM city_info_bucketed t1 LEFT JOIN measurement_lite_bucketed t2 ON t1.city_hash = t2.city_hash AND t2.measurement > 30 GROUP BY t1.province;
执行计划:
可以看到就是普通的map join。
bucket map join生效的条件在满足map join的条件上还需要:
-
按 Join Key 进行分桶 (Bucketing on Join Keys)
-
桶内按 Join Key 排序 (Sorting within Buckets)
-
桶数量成整数倍 (Bucket Count Relationship)
创建对应的分桶表并且按照join key排序
sqlCREATE TABLE city_info_smb ( city_id INT, city_name STRING, country STRING, province STRING, population BIGINT, longitude DECIMAL(10, 6), latitude DECIMAL(10, 6), post_code STRING, area_code STRING, created_at TIMESTAMP, city_hash INT ) -- 黄金法则: 1.按Join Key分桶; 2.桶内按Join Key排序 CLUSTERED BY (city_hash) SORTED BY (city_hash) INTO 32 BUCKETS STORED AS ORC; -- 创建 measurement_lite 的 SMB 版本 CREATE TABLE measurement_lite_smb ( city STRING, measurement DECIMAL(10,2), city_hash INT ) -- 黄金法则: 1.按Join Key分桶; 2.桶内按Join Key排序; 3.桶数量相等 CLUSTERED BY (city_hash) SORTED BY (city_hash) INTO 32 BUCKETS STORED AS ORC; EXPLAIN SELECT t1.province, COUNT(1) AS numbers -- 使用分桶键进行 Join FROM city_info_smb t1 LEFT JOIN measurement_lite_smb t2 ON t1.city_hash = t2.city_hash AND t2.measurement > 30 GROUP BY t1.province;
通过执行计划可以看出 Sorted Merge Bucket Map Join的特点:
在 map join的基础上省去了小表哈希表构建和广播的过程,从数据被读取(TableScan)到数据被连接(SMB Map Join Operator),中间完全没有 Reduce Output Operator。这证明了数据没有经过任何网络 Shuffle 就完成了 Join 操作
HIVE对JOIN策略的选择优先级
-
Sorted Merge Bucket (SMB) Join
-
Map Join (Broadcast Join)
-
Common Join (Shuffle Join / Sort-Merge Join)
EXPLAIN AUTHORIZATION 使用
查看授权信息,在不实际执行查询的情况下,告诉你当前用户是否有足够的权限来运行这条查询,并列出所有必需的权限
sqlINPUTS: mydb@measurements_partitions mydb@measurements_partitions@city_hash=-1022324546 mydb@measurements_partitions@city_hash=-1034724598 mydb@measurements_partitions@city_hash=-1041684677 mydb@measurements_partitions@city_hash=-1072080229 OUTPUTS: hdfs://hadoop102:8020/tmp/hive/atguigu/f9e3d1db-18e4-4f9b-bae0-b249b3524fe2/hive_2025-08-28_10-57-52_088_1123441659099374210-2/-mr-10001 CURRENT_USER: atguigu OPERATION: QUERY
EXPLAIN ANALYZE 使用
实际执行一个SQL查询,并在执行后返回一个带有真实运行时统计信息的详细执行计划
sqlexplain ANALYZE select city, count(1) from measurements_partitions t where city_hash in (-1414109858,-1512161185) group by t.city;
参考文档:
版权声明