Performance Considerations for Join Queries. 相对于修改物理因素,如文件格式或硬件配置,连接是你可以在 SQL 层级进行调整的主要方面(Joins are the main class of queries that you can tune at the SQL level, as opposed to changing physical factors such as the file format or the hardware configuration)。对于连接的性能,相关的主题 Column Statistics 和 Table Statistics 同样重要
Benchmarking Impala Queries. 用于 Impala 初始实验的配置和样本数据通常不适合进行性能测试(The configuration and sample data that you use for initial experiments with Impala is often not appropriate for doing performance tests)
Controlling Resource Usage. 更多内存,更加性能(The more memory Impala can utilize, the better query performance you can expect)。在一个同样运行其他负载的集群中,你必须权衡考虑,保证所有 Hadoop 组件具有能良好运行的足够内存,因此你可能限制 Impala 可使用的内存
分区表的所有数据文件默认放在一个目录下。分区是一项基于一个或多个上的值,在载入时物理拆分数据的技术。例如,对于根据 year 列分区的school_records 表来说,对于每一个不同的年份都有一个单独的数据目录,并且这一年的所有数据都存放在这个目录下的数据文件中。一个包含 WHERE 条件如 YEAR=1966, YEAR IN (1989,1999), YEAR BETWEEN 1984 AND 1989 的查询,可以只从对应的一个或多个目录下检索数据文件,极大的减少了读取和测试的数据的数量。 分区通常对应:
非常大的表,完整读取整个数据集花费的时间不可想象(where reading the entire data set takes an impractical amount of time)
全部或几乎所有的查询都包含分区列查询条件的表。我们上面的例子中那个根据 year 分区的表, SELECT COUNT(*) FROM school_records WHERE year = 1985 是高效的,只检索数据的一小部分;但是 SELECT COUNT(*) FROM school_records 则必须处理每一年的单独的数据文件,导致必未分区表更多的工作。假如你频繁基于 last name, student ID, 等等不检测年份的对表进行查询,考虑不分区表
列包含合理的基数(cardinality--不同值的个数)。假如列只包括少量的值,如 Male 或 Female,你无法通过对每一个查询消除大约 50% 数据的读取来获得更高的效率。假如列的每一个值只对应很少的行,要处理的目录的数量会变成一个限制因素,并且每一个目录中的数据文件可能太小了,无法从 Hadoop 以 multi-megabyte 块传输数据的机制受益。例如,你可能用年来分区人口数据,用年和月来存放销售数据,用年月日来分区网络流量数据(一些更高流量的用户甚至用小时和分钟来分区数据)
总是使用抽取、转换、加载(ETL)管道加载的数据。分区列的值从原始的数据文件剥离,并对应到目录名中,因此加载数据到分区表涉及了某种转换或预处理(The values of the partitioning columns are stripped from the original data files and represented by directory names, so loading data into a partitioned table involves some sort of transformation or preprocessing)
在 Impala SQL 语法中,分区会影响到这些语句:
CREATE TABLE: 在创建表时,使用 PARTITIONED BY 子句来标识分区列的名称和数据类型。表中的列不包括这些分区列
ALTER TABLE: 可以添加或删除分区,用于处理海量数据集的不同部分。对于根据日期值分区的数据,你可以不再保留"过期(age out)"的数据
分区修剪(Partition pruning)指的是一种查询可以跳过一个或多个分区对应的数据文件不进行读取的技术。假如你能安排你的查询从查询计划中剪除大量的不必要的分区,查询使用更少的资源,因此与剪除的不必要的分区成比例的变快,并且更可扩展(If you can arrange for queries to prune large numbers of unnecessary partitions from the query execution plan, the queries use fewer resources and are thus proportionally faster and more scalable)。
例如,如果一个表使用 YEAR, MONTH, DAY 分区,这样如 WHERE year = 2013, WHERE year < 2010, WHERE year BETWEEN 1995 AND 1998 等 WHERE 子句允许 Impala 除了指定范围的分区外,跳过所有其他分区的数据文件。同样的,WHERE year = 2013 AND month BETWEEN 1 AND 3 甚至可以剪除更多的分区,只读取一年中的一部分数据文件。
通过WHERE 子句中其他部分的中间属性,甚至在分区键列没有明确指定常量值的时候,Impala 都可以进行分区修剪(Impala can even do partition pruning in cases where the partition key column is not directly compared to a constant, by applying the transitive property to other parts of the WHERE clause)。这一技术称为谓词传播(predicate propagation),自 Impala 1.2.2 开始可用。在下面例子里,表 census 中包含另一个列存放数据收集的时间(是 10 年采集的)。即使分区键列 (YEAR) 没有对应一个常量, Impala 也可以推断只有 YEAR=2010 分区是必需的,并再次只读取了总分区中 1/3 个分区。
[localhost:21000] > drop table census;
[localhost:21000] > create table census (name string, census_year int) partitioned by (year int);
[localhost:21000] > insert into census partition (year=2010) values ('Smith',2010),('Jones',2010);
[localhost:21000] > insert into census partition (year=2011) values ('Smith',2020),('Jones',2020),('Doe',2020);
[localhost:21000] > insert into census partition (year=2012) values ('Smith',2020),('Doe',2020);
[localhost:21000] > select name from census where year = census_year and census_year=2010;
+-------+
| name |
+-------+
| Smith |
| Jones |
+-------+
[localhost:21000] > explain select name from census where year = census_year and census_year=2010;
在执行查询之后,立刻检查 PROFILE 语句的输出,了解实际读取和处理的数据量更详细的分析。
假如是在分区表上建立的视图,所有的分区修剪都是由原使得查询子句确定。即使在试图上的查询包含了引用分区键列的 WHERE 子句,Impala 不会修剪添加的列(If a view applies to a partitioned table, any partition pruning is determined by the clauses in the original query. Impala does not prune additional columns if the query on the view includes extra WHEREclauses referencing the partition key columns)。
Remember that when Impala queries data stored in HDFS, it is most efficient to use multi-megabyte files to take advantage of the HDFS block size. 对于 Parquet 表,块大小 (数据文件理想大小) 是 1GB。因此,应避免指定太多分区键列,这样会导致个别分区只包含少量数据。例如,假如你每天获取 1GB 数据,你可能使用年、月、日进行分区;当你每分钟获取 5GB 数据时,你可能使用年、月、日、时、分来分区。假如你的数据保护地理组件,假如你每个邮编都有很多M的数据时,你可以基于邮编分区;假如不是,那么你可能需要使用更大的区域,如 city, state, 或 country. state 分区。
调整 Impala 连接查询的最简单的技术就是在参与连接的每个表上使用 COMPUTE STATS 语句采集统计信息,然后让 Impala 基于每一个表的大小、每一个列不同值的个数、等等信息自动的优化查询。COMPUTE STATS 语句和 连接优化(join optimization)是 Impala 1.2.2 引入的新功能。为了保证每个表上统计信息的精确,请在表加载数据之后执行 COMPUTE STATS 语句,并在因 INSERT, LOAD DATA, 添加分区等操作导致数据大幅变化之后再次执行。假如连接查询中所有表的统计信息不可用,或 Impala 选择的连接顺序不是最优,你可以通过在 SELECT 关键字之后立刻紧跟 STRAIGHT_JOIN 关键字,来覆盖自动的连接顺序优化。这时候,Impala 使用表在查询中出现的顺序来指导连接如何处理。首先是最大的表,然后是次大的,依此类推。术语"最大"和"最小"指中间结果集的大小,这些基于作为结果集一部分的每个表的行数和列数(The terms "largest" and "smallest" refers to the size of the intermediate result set based on the number of rows and columns from each table that are part of the result set)。例如,如果你连接了表sales 和 customers,查询可能是从产生了 5000 次购买的 100 个用户中查找结果集。这时候,你应该使用 SELECT ... FROM sales JOIN customers ..., 把 customers 放在右侧,因为在这个查询上下文中它更小。依赖于表的绝对和相对的大小,Impala 查询计划器在执行连接查询的不同技术之间进行选择。广播连接(Broadcast joins) 是默认方式,右侧的表被认为比左侧的表小,并且它的内容被发送到查询涉及到的其他节点上。替代的技术称作分割连接(partitioned join) (与分区表无关),更适用于近乎相同大小的大型表的连接。使用这一技术,每一个表的部分内容被发送到对应的其他节点,然后这些行的子集可以并行处理。广播和分区连接的选择仍然依赖于连接中所有表的可用的、使用 COMPUTE STATS 语句手机的统计信息。对查询执行 EXPLAIN 语句,查看该查询采用了哪种连接策略。如果你发现一个查询使用了广播连接,而你通过基准测试知道分割连接更高效,或者相反情况时,在查询上添加提示指定使用的精确的连接机制。参见 Hints 了解详细信息。
统计信息不可用时连接如何处理
假如连接中的一些表的表或列统计信息不可用,Impala 仍然使用可用的那部分信息重新排列表,包含可用统计信息的表放在连接的左侧,按照整体大小和基数降序排列(Tables with statistics are placed on the left side of the join order, in descending order of cost based on overall size and cardinality)。没有统计信息的表被认为大小为 0,也就是说,它们总是放置在连接查询的右侧。
Overriding Join Reordering with STRAIGHT_JOIN 假如因为过时的统计信息或意外的数据分布, Impala 连接查询很低效,你可以通过在 SELECT 关键字之后紧跟着 STRAIGHT_JOIN 关键字来重新排序连接的表,使的 Impala 高效。STRAIGHT_JOIN 关键字关闭 Impala 内部使用的连接子句的重新排序,并根据 查询中 join 子句中列出的顺序优化(The STRAIGHT_JOIN keyword turns off the reordering of join clauses that Impala does internally, and produces a plan that relies on the join clauses being ordered optimally in the query text)。这时,重写查询以便最大的表在最左侧,跟着是次大的,依此类推直到最小的表放在最右侧。
在下面的例子里,基于 BIG 表的子查询产生一个非常小的结果集,但是这个表仍被视为好像它是最大的并放置在连接顺序的第一位。为最后的连接子句使用 STRAIGHT_JOIN 关键字,防止最终的表重新排序,保持它作为最右边表的连接顺序(Using STRAIGHT_JOIN for the last join clause prevents the final table from being reordered, keeping it as the rightmost table in the join order)。
select straight_join x from medium join small join (select * from big where c1 < 10) as big
where medium.id = small.id and small.id = big.id;
复制代码
Examples of Join Order Optimization下面的例子演示了10亿、2亿、1百万行表之间的连接(这时,表都是未分区的,使用 Parquet 格式)。最小的表是最大的表的一个子集,方便起见在唯一的 ID 列上进行连接。最小的表只包含其他表中列的一个子集。
[localhost:21000] > create table big stored as parquet as select * from raw_data;
+----------------------------+
| summary |
+----------------------------+
| Inserted 1000000000 row(s) |
+----------------------------+
Returned 1 row(s) in 671.56s
[localhost:21000] > desc big;
+-----------+---------+---------+
| name | type | comment |
+-----------+---------+---------+
| id | int | |
| val | int | |
| zfill | string | |
| name | string | |
| assertion | boolean | |
+-----------+---------+---------+
Returned 5 row(s) in 0.01s
[localhost:21000] > create table medium stored as parquet as select * from big limit 200 * floor(1e6);
+---------------------------+
| summary |
+---------------------------+
| Inserted 200000000 row(s) |
+---------------------------+
Returned 1 row(s) in 138.31s
[localhost:21000] > create table small stored as parquet as select id,val,name from big where assertion = true limit 1 * floor(1e6);
+-------------------------+
| summary |
+-------------------------+
| Inserted 1000000 row(s) |
+-------------------------+
Returned 1 row(s) in 6.32s
复制代码
对于任意类型的性能测试,使用 EXPLAIN 语句查看将执行的查询是如何的昂贵(expensive)而不需要实际运行它,并且启用详细的 EXPLAIN 计划包含更详细的性能导向的信息:最有趣的计划行---展示了没有统计信息的连接的表--以黑体突出,Impala 无法正确的估算处理的每个阶段中涉及的行数,通常采用广播连接机制把其中之一的表的完整数据发送到各个节点上(Impala cannot make a good estimate of the number of rows involved at each stage of processing,and is likely to stick with the BROADCAST join mechanism that sends a complete copy of one of the tables to each node)。
[localhost:21000] > set explain_level=verbose;
EXPLAIN_LEVEL set to verbose
[localhost:21000] > explain select count(*) from big join medium where big.id = medium.id;
对于特定的一组列,使用 SHOW COLUMN STATS table_name 语句检查列统计信息是否可用,或检查针对引用这系列的表的查询的扩展的 EXPLAIN 输出。参见 SHOW 语句 和 EXPLAIN 语句了解详细信息。
通过 ALTER TABLE 手工设置统计信息 所有统计信息中最关键的部分是表(未分区的表)或分区(分区表)中的行数。COMPUTE STATS 语句总是采集所有列的统计信息以及整个表的统计信息。假如在添加了一个分区或插入数据之后,进行完整的 COMPUTE STATS 操作实际不可行时,或者当行数不同时,可以预见 Impala 将产生更好的执行计划时,你可以通过 ALTER TABLE 语句手工设置行数:
create table analysis_data stored as parquet as select * from raw_data;
Inserted 1000000000 rows in 181.98s
compute stats analysis_data;
insert into analysis_data select * from smaller_table_we_forgot_before;
Inserted 1000000 rows in 15.32s
-- 现在表里共有 1001000000 行。我们可以更新统计信息中的这一个数据点
alter table analysis_data set tblproperties('numRows'='1001000000');
复制代码
对于分区表,同时更新每一个分区的行数和整个表的行数:
-- 如果原来表中包含 1000000 行,我们新添加了一个分区
-- 修改该分区和整个表的 numRows 属性
alter table partitioned_data partition(year=2009, month=4) set tblproperties ('numRows'='30000');
alter table partitioned_data set tblproperties ('numRows'='1030000');
复制代码
实际上,COMPUTE STATS 语句已经够快了,这一技术是不必要的。这一方法最大的价值就是可以调整 numRows 值的大小来产生理想的连接顺序从而解决性能问题(It is most useful as a workaround for in case of performance issues where you might adjust the numRowsvalue higher or lower to produce the ideal join order)。