前言
本文为笔者个人阅读Apache Impala源码时的笔记,仅代表我个人对代码的理解,个人水平有限,文章可能存在理解错误、遗漏或者过时之处。如果有任何错误或者有更好的见解,欢迎指正。
基本信息
在Impala中,Stats记录了一张表的大小、行数等统计信息,这些信息存储在metastore中,并被Impala用来帮助优化查询,包括以下几点:
- 准确的统计信息可以让Impala为JOIN查询构建高效的查询计划,提高性能并减少内存使用。
- 准确的统计信息可以让Impala为Parquet表的INSERT更有效地分配负载,提高性能并减少内存使用。
- 准确的统计信息有助于Impala估计每个查询所需的内存,这在使用资源管理特性时非常重要,如在准入控制和YARN资源管理框架中,统计信息帮助Impala实现高并发、内存充分利用,并避免与其他Hadoop组件的竞争资源。
由于许多最关键的性能和资源密集型操作依赖于表和列统计信息来构建准确和高效的执行计划,因此COMPUTE STATS是ETL过程末尾的一个重要步骤。
一、统计信息的查看
可以通过如下SQL语句查看统计信息:
-- 查看表级别统计信息
SHOW TABLE STATS [db_name.]table_name
-- 查看列级别统计信息
SHOW COLUMN STATS [db_name.]table_name
SHOW TABLE STATS和SHOW COLUMN STATS变量对于调优性能和诊断性能问题非常重要,特别是对于大表和复杂的JOIN查询。统计信息中任何未知的值(因为没有进行统计信息计算)都显示为-1。
二、统计信息的计算
可以通过如下SQL语句计算与删除统计信息:
-- 计算一张表的统计信息,可以指定只计算某些列,如果没有给出列列表,则计算表中所有列的列级统计信息
COMPUTE STATS [db_name.]table_name [ ( column_list ) ] [TABLESAMPLE SYSTEM(percentage) [REPEATABLE(seed)]]
column_list ::= column_name [ , column_name, ... ]
-- 增量计算统计信息,可以指定只计算某些分区,如果没有指定分区,则计算表中所有分区
COMPUTE INCREMENTAL STATS [db_name.]table_name [PARTITION (partition_spec)]
partition_spec ::= simple_partition_spec | complex_partition_spec
simple_partition_spec ::= partition_col=constant_value
complex_partition_spec ::= comparison_expression_on_partition_col
-- 删除统计信息
DROP STATS [database_name.]table_name
-- 删除增量统计信息,必须指定分区
DROP INCREMENTAL STATS [database_name.]table_name PARTITION (partition_spec)
partition_spec ::= partition_col=constant_value
可选的TABLESAMPLE子句指定COMPUTE STATS操作只处理指定百分比的表数据。对于太大而无法进行完整的COMPUTE STATS操作的表,可以使用带有TABLESAMPLE子句的COMPUTE STATS从表数据样本推断统计信息。PARTITION子句只允许与INCREMENTAL子句结合使用,且对于COMPUTE INCREMENTAL STATS是可选的,对于DROP INCREMENTAL STATS则是必需的。
执行COMPUTE STATS后Impala会自己执行另外一个SELECT查询来计算统计信息,如图所示:
需要注意的是,对于一张表,可以使用COMPUTE STATS或COMPUTE INCREMENTAL STATS,但不要混用或者交替使用两者。如果在一个表的生命周期内从COMPUTE STATS切换到COMPUTE INCREMENTAL STATS,或者反之,应当在切换之前先运行DROP STATS删除所有统计信息。
另外,第一次对一个表执行增量统计计算时,无论表是否已经有统计信息,统计信息都将从头开始重新计算,因此,当第一次在给定的表上运行COMPUTE INCREMENTAL STATS时,需要进行一次性的资源密集型操作来扫描整个表。
COMPUTE INCREMENTAL STATS只适用于分区表。如果对非分区表使用INCREMENTAL子句,Impala会自动使用COMPUTE STATS语句。这样的表在SHOW TABLE STATS输出的Incremental stats列下显示为false。
三、统计信息的检查
在开始制定查询执行计划时,在Frontend.java的createPlanExecInfo
方法里会遍历所有本次查询需要Scan的表,逐一检查统计信息,以下是略去无关逻辑的部分代码:
// 缺失统计信息的表集合
Set<TTableName> tablesMissingStats = Sets.newTreeSet();
// 统计信息损坏的表集合
Set<TTableName> tablesWithCorruptStats = Sets.newTreeSet();
// 遍历每个ScanNode,ScanNode和本次查询要扫描的表一一对应
for (ScanNode scanNode: scanNodes) {
TTableName tableName = scanNode.getTupleDesc().getTableName().toThrift();
// 分别检查统计信息是否有缺失、损坏以及磁盘ID是否缺失,并将表名加入对应集合
if (scanNode.isTableMissingStats()) tablesMissingStats.add(tableName);
if (scanNode.hasCorruptTableStats()) tablesWithCorruptStats.add(tableName);
}
统计信息缺失
首先看统计信息缺失的检查,判断统计信息是否缺失使用了isTableMissingStats
,可以发现统计信息缺失的判断包括两部分,分别是列统计信息的判断isTableMissingColumnStats
和表统计信息的判断isTableMissingTableStats
,两者任一缺失,该表都被判断为缺失了统计信息,相关代码如下:
// 如果此扫描下的表缺少与此扫描节点相关的表状态或列状态,则返回true。
public boolean isTableMissingStats() {
return isTableMissingColumnStats() || isTableMissingTableStats();
}
// 如果有列没有统计信息,则返回true,复杂类型的列将被跳过。
public boolean isTableMissingColumnStats() {
for (SlotDescriptor slot: desc_.getSlots()) {
if (slot.getColumn() != null && !slot.getStats().hasStats() &&
!slot.getColumn().getType().isComplexType()) {
return true;
}
}
return false;
}
// ScanNode类的isTableMissingTableStats,如果表没有统计信息,则返回true
public boolean isTableMissingTableStats() {
return desc_.getTable().getNumRows() == -1;
}
在列的统计信息检查isTableMissingColumnStats
中可以发现其遍历了所有Slot,复杂类型的列(Struct、Map和Array)会被跳过。Slot包括具名和匿名两种,具名Slot可理解为SQL中SELECT的字段,匿名Slot为一些查询中间结果,如聚合运算。用Slot的getStats
方法获取对应列的列统计信息ColumnStats
并用其成员方法hasStats
检查是否具有统计信息,也就是说列统计信息的检查只包括查询相关的列而非该表所有列,列统计信息由ColumnStats
类定义,具体内容包括一列的空值个数、不同值个数、平均字节数和最大字节数。hasStats
判断逻辑如下,即空值个数和不同值个数二者有其一已知(不为-1)即视为有列统计信息:
public boolean hasStats() { return numNulls_ != -1 || numDistinctValues_ != -1; }
表的统计信息由TTableStats
定义,内容包括表的行数和文件大小。在isTableMissingTableStats
中通过FeTable
的getNumRows
方法获取表行数,并判断该表的行数是否已知(即不为-1),已知表行数即认为该表有统计信息。HdfsScanNode
类重写了isTableMissingTableStats
方法:
// HdfsScanNode类重写的isTableMissingTableStats
@Override
public boolean isTableMissingTableStats() {
// 使用了推测行数时不检查
if (extrapolatedNumRows_ >= 0) return false;
// NumClusteringCols即分区键的数量,该值大于0说明是分区表
if (tbl_.getNumClusteringCols() > 0
// numPartitionsWithNumRows_为有NumRows的分区数量
// 若与要扫描的分区数量不等,则说明有分区缺失了统计信息
&& numPartitionsWithNumRows_ != partitions_.size()) {
return true;
}
// 非分区表则调用父类方法,检查表级别的统计信息
return super.isTableMissingTableStats();
}
可以发现对于分区表,表统计信息的检查只包括查询所涉及的分区而非该表所有分区,且任意一个涉及分区缺失统计信息都会判断为缺失表统计信息。
统计信息损坏
然后是统计信息损坏的情况,由hasCorruptTableStats
检查,该方法在ScanNode
中永远返回False,我们只看其在HdfsScanNode
中的重写:
// 如果扫描的表被怀疑有损坏的统计信息,特别是当扫描为非空且numRows为0或负(但不是-1)时,返回true。
@Override
public boolean hasCorruptTableStats() { return hasCorruptTableStats_; }
// 指示是否有损坏的表统计信息,当ScanRange非空并且numRows为0时,设置为True。
private boolean hasCorruptTableStats_;
hasCorruptTableStats_
为True时即统计信息损坏,该值主要在getStatsNumRows
方法中被设置,以下为关键代码:
private long getStatsNumRows(TQueryOptions queryOptions) {
...
hasCorruptTableStats_ = false;
// 对于分区表,检查每个涉及的分区
if (tbl_.getNumClusteringCols() > 0) {
for (FeFsPartition p: partitions_) {
long partNumRows = p.getNumRows();
// 当该分区numRows为非-1的负数、或numRows为0而分区却有数据时,可以认为该分区统计信息损坏
if (partNumRows < -1 || (partNumRows == 0 && p.getSize() > 0)) {
hasCorruptTableStats_ = true;
}
...
// 当numRows为非-1的负数、或numRows为0而表却有数据时,可以认为统计信息损坏
if (numRows < -1 || (numRows == 0 && tbl_.getTotalHdfsBytes() > 0)) {
hasCorruptTableStats_ = true;
}
...
}
总结一下,统计信息的损坏包括两种情况,一是numRows
出现非-1负数,-1代表行数未知,非-1负数则是异常情况,二是numRows
为0,表中却有数据,说明统计信息与表数据不匹配,这可能是表新增了数据,统计信息过时了。对于分区表,且任意一个查询涉及的分区统计信息损坏都会判断为表统计信息损坏。
无论统计信息缺失或者损坏都不会直接影响查询的执行,但是没有统计信息可能会影响查询性能,例如,缺失统计信息可能导致impala制定不合理的执行计划(如Join时广播大表),进而影响性能并占用更多资源。
当出现表统计信息缺失或损坏时,我们可以从查询的profile中得知,若有"Tables Missing Stats"字段则表示出现了统计信息缺失,并会在其后打印出表名。对于损坏的统计信息则是"Tables With Corrupt Table Stats"字段,其他同理。