Impala教程
本节包括教程场景,演示软件安装后如何开始使用Impala。它的重点是加载数据的技术,因为一旦你在表中有了一些数据,并且可以查询这些数据,你就可以快速地发展到更高级的Impala功能。
注意:
在可行的情况下,本教程将带你从 "零起点 "到拥有所需的Impala表格和数据。在某些情况下,您可能需要从外部来源下载额外的文件,设置额外的软件组件,修改命令或脚本以适合您自己的配置,或替换您自己的样本数据。
在尝试这些教程课程之前,请使用这些程序之一安装Impala。
- 如果你已经设置了一些Apache Hadoop环境,只需要将Impala添加到其中,请按照安装Impala中描述的安装过程进行安装。如果你还没有配置Hive,请确保也安装Hive metastore服务。
入门教程
这些教程演示了使用Impala的基础知识。这些教程是为首次使用Impala的用户准备的,也是为了在任何新的集群上试用Impala,以确保主要组件工作正常。
探索新的 Impala 实例
本教程演示了如何在陌生(可能是空的)Impala实例的表和数据库中寻找方法的技术。
当您第一次连接到Impala实例时,您使用SHOW DATABASES和SHOW TABLES语句来查看最常见的对象类型。另外,调用version()函数确认您正在运行的Impala是哪个版本;在查阅文档和处理支持问题时,版本号很重要。
一个完全空的Impala实例不包含任何表,但仍然有两个数据库。
- default,当你不指定任何其他数据库时,就会创建新表。
- _impala_builtins,一个系统数据库,用来存放所有内置函数。
下面的例子显示了如何查看可用的数据库,以及每个数据库中的表。如果数据库或表的列表很长,可以使用通配符符号根据它们的名称来定位特定的数据库或表。
$ impala-shell -i localhost --quiet Starting Impala Shell without Kerberos authentication Welcome to the Impala shell. Press TAB twice to see a list of available commands. ... (Shell build version: Impala Shell v3.4.x (hash) built on date) [localhost:21000] > select version(); +------------------------------------------- | version() +------------------------------------------- | impalad version ... | Built on ... +------------------------------------------- [localhost:21000] > show databases; +--------------------------+ | name | +--------------------------+ | _impala_builtins | | ctas | | d1 | | d2 | | d3 | | default | | explain_plans | | external_table | | file_formats | | tpc | +--------------------------+ [localhost:21000] > select current_database(); +--------------------+ | current_database() | +--------------------+ | default | +--------------------+ [localhost:21000] > show tables; +-------+ | name | +-------+ | ex_t | | t1 | +-------+ [localhost:21000] > show tables in d3; [localhost:21000] > show tables in tpc; +------------------------+ | name | +------------------------+ | city | | customer | | customer_address | | customer_demographics | | household_demographics | | item | | promotion | | store | | store2 | | store_sales | | ticket_view | | time_dim | | tpc_tables | +------------------------+ [localhost:21000] > show tables in tpc like 'customer*'; +-----------------------+ | name | +-----------------------+ | customer | | customer_address | | customer_demographics | +-----------------------+
一旦知道了有哪些表和数据库,就会用USE语句降入数据库。为了了解每个表的结构,你可以使用DESCRIBE命令。一旦进入数据库,就可以发出 INSERT 和 SELECT 等语句,对特定的表进行操作。
下面的例子探讨了一个名为 TPC 的数据库,它的名字我们在前面的例子中已经学过了。它展示了如何根据搜索字符串过滤数据库内的表名,检查表的列,以及运行查询来检查表数据的特性。例如,对于一个不熟悉的表,你可能想知道行数、列的不同值的数量,以及其他属性,如列是否包含任何NULL值。从表中抽取实际数据值时,如果表中包含的行数或不同值比你预期的要多,请使用limit子句来避免过多的输出。
[localhost:21000] > use tpc;
[localhost:21000] > show tables like '*view*';
+-------------+
| name |
+-------------+
| ticket_view |
+-------------+
[localhost:21000] > describe city;
+-------------+--------+---------+
| name | type | comment |
+-------------+--------+---------+
| id | int | |
| name | string | |
| countrycode | string | |
| district | string | |
| population | int | |
+-------------+--------+---------+
[localhost:21000] > select count(*) from city;
+----------+
| count(*) |
+----------+
| 0 |
+----------+
[localhost:21000] > desc customer;
+------------------------+--------+---------+
| name | type | comment |
+------------------------+--------+---------+
| c_customer_sk | int | |
| c_customer_id | string | |
| c_current_cdemo_sk | int | |
| c_current_hdemo_sk | int | |
| c_current_addr_sk | int | |
| c_first_shipto_date_sk | int | |
| c_first_sales_date_sk | int | |
| c_salutation | string | |
| c_first_name | string | |
| c_last_name | string | |
| c_preferred_cust_flag | string | |
| c_birth_day | int | |
| c_birth_month | int | |
| c_birth_year | int | |
| c_birth_country | string | |
| c_login | string | |
| c_email_address | string | |
| c_last_review_date | string | |
+------------------------+--------+---------+
[localhost:21000] > select count(*) from customer;
+----------+
| count(*) |
+----------+
| 100000 |
+----------+
[localhost:21000] > select count(distinct c_birth_month) from customer;
+-------------------------------+
| count(distinct c_birth_month) |
+-------------------------------+
| 12 |
+-------------------------------+
[localhost:21000] > select count(*) from customer where c_email_address is null;
+----------+
| count(*) |
+----------+
| 0 |
+----------+
[localhost:21000] > select distinct c_salutation from customer limit 10;
+--------------+
| c_salutation |
+--------------+
| Mr. |
| Ms. |
| Dr. |
| |
| Miss |
| Sir |
| Mrs. |
+--------------+
当你从只读探索毕业后,你会使用CREATE DATABASE和CREATE TABLE等语句来设置自己的数据库对象。
下面的例子演示了创建一个持有新表的新数据库。虽然上一个例子是在TPC数据库内部结束的,但新的EXPERIMENTS数据库并没有嵌套在TPC内部,所有的数据库都被安排在一个顶层列表中。
[localhost:21000] > create database experiments;
[localhost:21000] > show databases;
+--------------------------+
| name |
+--------------------------+
| _impala_builtins |
| ctas |
| d1 |
| d2 |
| d3 |
| default |
| experiments |
| explain_plans |
| external_table |
| file_formats |
| tpc |
+--------------------------+
[localhost:21000] > show databases like 'exp*';
+---------------+
| name |
+---------------+
| experiments |
| explain_plans |
+---------------+
下面的例子创建了一个新表T1。为了说明一个常见的错误,它在错误的数据库中创建了这个表,也就是前一个例子结束的TPC数据库。ALTER TABLE 语句可以让您将表移动到预定的数据库 EXPERIMENTS 中,作为重命名操作的一部分。在切换到新的数据库时,总是需要使用USE语句,并使用current_database()函数确认会话在哪个数据库中,以避免这类错误。
[localhost:21000] > create table t1 (x int);
[localhost:21000] > show tables;
+------------------------+
| name |
+------------------------+
| city |
| customer |
| customer_address |
| customer_demographics |
| household_demographics |
| item |
| promotion |
| store |
| store2 |
| store_sales |
| t1 |
| ticket_view |
| time_dim |
| tpc_tables |
+------------------------+
[localhost:21000] > select current_database();
+--------------------+
| current_database() |
+--------------------+
| tpc |
+--------------------+
[localhost:21000] > alter table t1 rename to experiments.t1;
[localhost:21000] > use experiments;
[localhost:21000] > show tables;
+------+
| name |
+------+
| t1 |
+------+
[localhost:21000] > select current_database();
+--------------------+
| current_database() |
+--------------------+
| experiments |
+--------------------+
对于你最初的表格实验,你可以使用只有几列和几行的表格,以及文本格式的数据文件。
注意:当你升级到更现实的场景时,你将会使用更复杂的表,有很多列,有分区等功能,还有Parquet等文件格式。在处理现实的数据量时,你将使用LOAD DATA或INSERT......SELECT语句引入数据,一次对数百万或数十亿行进行操作。
下面的例子设置了几个简单的表,只有几行,并执行涉及排序、聚合函数和连接的查询。
[localhost:21000] > insert into t1 values (1), (3), (2), (4);
[localhost:21000] > select x from t1 order by x desc;
+---+
| x |
+---+
| 4 |
| 3 |
| 2 |
| 1 |
+---+
[localhost:21000] > select min(x), max(x), sum(x), avg(x) from t1;
+--------+--------+--------+--------+
| min(x) | max(x) | sum(x) | avg(x) |
+--------+--------+--------+--------+
| 1 | 4 | 10 | 2.5 |
+--------+--------+--------+--------+
[localhost:21000] > create table t2 (id int, word string);
[localhost:21000] > insert into t2 values (1, "one"), (3, "three"), (5, 'five');
[localhost:21000] > select word from t1 join t2 on (t1.x = t2.id);
+-------+
| word |
+-------+
| one |
| three |
+-------+
完成本教程后,你现在应该知道。
- 如何判断系统上运行的是哪个版本的Impala。
- 如何查找Impala实例中的数据库名称,显示完整列表或搜索特定名称。
- 如何查找Impala数据库中表的名称,显示完整列表或搜索特定名称。
- 如何在数据库之间进行切换,并检查当前所在的数据库。
- 如何学习表的列名和类型。
- 如何创建数据库和表,插入少量测试数据,并运行简单的查询。
从本地文件加载CSV数据
这个方案说明了如何创建一些非常小的表,适合初次使用Impala SQL功能的用户进行实验。TAB1和TAB2被加载了来自HDFS中文件的数据。将TAB1中的数据子集复制到TAB3中。
用您要查询的数据填充HDFS。要开始这个过程,在HDFS中的用户目录下创建一个或多个新的子目录。每个表的数据都驻留在一个单独的子目录中。在适当的地方用自己的用户名代替用户名。这个例子使用-p选项和mkdir操作来创建任何必要的父目录,如果它们还不存在的话。
$ whoami
username
$ hdfs dfs -ls /user
Found 3 items
drwxr-xr-x - username username 0 2013-04-22 18:54 /user/username
drwxrwx--- - mapred mapred 0 2013-03-15 20:11 /user/history
drwxr-xr-x - hue supergroup 0 2013-03-15 20:10 /user/hive
$ hdfs dfs -mkdir -p /user/username/sample_data/tab1 /user/username/sample_data/tab2
下面是一些示例数据,对于两个名为TAB1和TAB2的表格。
将以下内容复制到本地文件系统的.csv文件中。
tab1.csv:
1,true,123.123,2012-10-24 08:55:00
2,false,1243.5,2012-10-25 13:40:00
3,false,24453.325,2008-08-22 09:33:21.123
4,false,243423.325,2007-05-12 22:32:21.33454
5,true,243.325,1953-04-22 09:11:33
tab2.csv:
1,true,12789.123
2,false,1243.5
3,false,24453.325
4,false,2423.3254
5,true,243.325
60,false,243565423.325
70,true,243.325
80,false,243423.325
90,true,243.325
使用以下命令将每个.csv文件放入一个单独的HDFS目录中,这些命令使用Impala Demo VM中的可用路径。
$ hdfs dfs -put tab1.csv /user/username/sample_data/tab1
$ hdfs dfs -ls /user/username/sample_data/tab1
Found 1 items
-rw-r--r-- 1 username username 192 2013-04-02 20:08 /user/username/sample_data/tab1/tab1.csv
$ hdfs dfs -put tab2.csv /user/username/sample_data/tab2
$ hdfs dfs -ls /user/username/sample_data/tab2
Found 1 items
-rw-r--r-- 1 username username 158 2013-04-02 20:09 /user/username/sample_data/tab2/tab2.csv
每个数据文件的名称并不重要。事实上,当Impala第一次检查数据目录的内容时,它认为目录中的所有文件都构成了表的数据,而不管有多少文件,也不管文件的名字是什么。
要了解自己的HDFS文件系统内有哪些路径,以及各种目录和文件的权限是什么,可以发出hdfs dfs -ls /,然后沿着树的方向对各种目录进行-ls操作。
使用 impala-shell 命令来创建表,可以是交互式的,也可以是通过 SQL 脚本。
下面的例子显示了创建三个表。对于每个表,示例显示了创建具有各种属性的列,如布尔或整数类型。该示例还包括提供数据格式化信息的命令,例如以逗号结束的行,这在从.csv文件导入数据的情况下很有意义。当我们在HDFS目录树中已经有包含数据的.csv文件时,我们指定包含相应.csv文件的目录位置。Impala认为该目录中所有文件的所有数据都代表了表的数据。
DROP TABLE IF EXISTS tab1;
-- The EXTERNAL clause means the data is located outside the central location
-- for Impala data files and is preserved when the associated Impala table is dropped.
-- We expect the data to already exist in the directory specified by the LOCATION clause.
CREATE EXTERNAL TABLE tab1
(
id INT,
col_1 BOOLEAN,
col_2 DOUBLE,
col_3 TIMESTAMP
)
ROW FORMAT DELIMITED FIELDS TERMINATED BY ','
LOCATION '/user/username/sample_data/tab1';
DROP TABLE IF EXISTS tab2;
-- TAB2 is an external table, similar to TAB1.
CREATE EXTERNAL TABLE tab2
(
id INT,
col_1 BOOLEAN,
col_2 DOUBLE
)
ROW FORMAT DELIMITED FIELDS TERMINATED BY ','
LOCATION '/user/username/sample_data/tab2';
DROP TABLE IF EXISTS tab3;
-- Leaving out the EXTERNAL clause means the data will be managed
-- in the central Impala data directory tree. Rather than reading
-- existing data files when the table is created, we load the
-- data after creating the table.
CREATE TABLE tab3
(
id INT,
col_1 BOOLEAN,
col_2 DOUBLE,
month INT,
day INT
)
ROW FORMAT DELIMITED FIELDS TERMINATED BY ',';
注意:成功通过这些CREATE TABLE语句是一个重要的验证步骤,以确认一切都正确配置了Hive元存储和HDFS权限。如果你在CREATE TABLE语句中收到任何错误。
确保你严格按照安装说明,在安装Impala中。
确保hive.metastore.Warehouse.dir属性指向一个Impala可以写入的目录。所有权应该是hive:hive,并且impala用户也应该是hive组的成员。
将Impala表指向现有的数据文件
设置Impala访问数据的一个方便方法是使用外部表,数据已经存在于一组HDFS文件中,你只需将Impala表指向包含这些文件的目录。例如,你可以在impala-shell中运行一个*.sql文件,内容类似于下面,创建一个Impala表,访问Hive使用的现有数据文件。
下面的例子设置了2个表,引用了Impala的TPC-DS样本包中的路径和样本数据。由于历史原因,数据物理上驻留在/user/hive下的HDFS目录树中,尽管这个特殊的数据完全由Impala而不是Hive管理。当我们创建一个外部表时,我们指定包含一个或多个数据文件的目录,Impala会查询该目录内所有文件的综合内容。下面是我们如何检查HDFS文件系统内的目录和文件。
$ cd ~/username/datasets
$ ./tpcds-setup.sh
... Downloads and unzips the kit, builds the data and loads it into HDFS ...
$ hdfs dfs -ls /user/hive/tpcds/customer
Found 1 items
-rw-r--r-- 1 username supergroup 13209372 2013-03-22 18:09 /user/hive/tpcds/customer/customer.dat
$ hdfs dfs -cat /user/hive/tpcds/customer/customer.dat | more
1|AAAAAAAABAAAAAAA|980124|7135|32946|2452238|2452208|Mr.|Javier|Lewis|Y|9|12|1936|CHILE||Javie
r.Lewis@VFAxlnZEvOx.org|2452508|
2|AAAAAAAACAAAAAAA|819667|1461|31655|2452318|2452288|Dr.|Amy|Moses|Y|9|4|1966|TOGO||Amy.Moses@
Ovk9KjHH.com|2452318|
3|AAAAAAAADAAAAAAA|1473522|6247|48572|2449130|2449100|Miss|Latisha|Hamilton|N|18|9|1979|NIUE||
Latisha.Hamilton@V.com|2452313|
4|AAAAAAAAEAAAAAAA|1703214|3986|39558|2450030|2450000|Dr.|Michael|White|N|7|6|1983|MEXICO||Mic
hael.White@i.org|2452361|
5|AAAAAAAAFAAAAAAA|953372|4470|36368|2449438|2449408|Sir|Robert|Moran|N|8|5|1956|FIJI||Robert.
Moran@Hh.edu|2452469|
...
下面是一个SQL脚本,用于设置Impala表,指向HDFS中的一些数据文件。 (虚拟机中的脚本通过Hive设置这样的表,在本演示中忽略这些表)。将以下内容保存为customer_setup.sql。
--
-- store_sales fact table and surrounding dimension tables only
--
create database tpcds;
use tpcds;
drop table if exists customer;
create external table customer
(
c_customer_sk int,
c_customer_id string,
c_current_cdemo_sk int,
c_current_hdemo_sk int,
c_current_addr_sk int,
c_first_shipto_date_sk int,
c_first_sales_date_sk int,
c_salutation string,
c_first_name string,
c_last_name string,
c_preferred_cust_flag string,
c_birth_day int,
c_birth_month int,
c_birth_year int,
c_birth_country string,
c_login string,
c_email_address string,
c_last_review_date string
)
row format delimited fields terminated by '|'
location '/user/hive/tpcds/customer';
drop table if exists customer_address;
create external table customer_address
(
ca_address_sk int,
ca_address_id string,
ca_street_number string,
ca_street_name string,
ca_street_type string,
ca_suite_number string,
ca_city string,
ca_county string,
ca_state string,
ca_zip string,
ca_country string,
ca_gmt_offset float,
ca_location_type string
)
row format delimited fields terminated by '|'
location '/user/hive/tpcds/customer_address';
我们可以用这样的命令来运行这个脚本:
impala-shell -i localhost -f customer_setup.sql
描述一下Impala表
现在你已经更新了Impala缓存的数据库元数据,你可以确认预期的表可以被Impala访问,并检查其中一个表的属性。我们在名为default的数据库中创建了这些表。如果这些表是在default以外的数据库中,我们将在检查或查询其表之前,发出命令use db_name来切换到该数据库。我们也可以通过在数据库名称前加注表名来限定表名,例如default.customer和default.customer_name。
[impala-host:21000] > show databases
Query finished, fetching results ...
default
Returned 1 row(s) in 0.00s
[impala-host:21000] > show tables
Query finished, fetching results ...
customer
customer_address
Returned 2 row(s) in 0.00s
[impala-host:21000] > describe customer_address
+------------------+--------+---------+
| name | type | comment |
+------------------+--------+---------+
| ca_address_sk | int | |
| ca_address_id | string | |
| ca_street_number | string | |
| ca_street_name | string | |
| ca_street_type | string | |
| ca_suite_number | string | |
| ca_city | string | |
| ca_county | string | |
| ca_state | string | |
| ca_zip | string | |
| ca_country | string | |
| ca_gmt_offset | float | |
| ca_location_type | string | |
+------------------+--------+---------+
Returned 13 row(s) in 0.01
查询Impala表
你可以查询表中包含的数据。Impala 根据您的配置,在单个节点或多个节点上协调查询执行,而无需运行 MapReduce 作业来执行中间处理的开销。
在Impala上执行查询有多种方式。
- 在交互模式下使用impala-shell命令。
$ impala-shell -i impala-host Connected to localhost:21000 [impala-host:21000] > select count(*) from customer_address; 50000 Returned 1 row(s) in 0.37s
- 传递文件中包含的一组命令。
$ impala-shell -i impala-host -f myquery.sql Connected to localhost:21000 50000 Returned 1 row(s) in 0.19s
- 向impala-shell命令传递一条命令。查询被执行,结果被返回,shell退出。确保引用命令,最好用单引号,以避免shell扩展字符,如*。
$ impala-shell -i impala-host -q 'select count(*) from customer_address' Connected to localhost:21000 50000 Returned 1 row(s) in 0.29s
数据加载和查询示例
本节介绍如何创建一些示例表并将数据加载到其中。然后可以使用Impala shell对这些表进行查询。
加载数据
加载数据包括:
- 建立一个数据集。下面的例子使用.csv文件。
- 创建要加载数据的表。
- 将数据加载到您创建的表中。
示例查询
要运行这些示例查询,请创建一个SQL查询文件query.sql,将每个查询复制并粘贴到查询文件中,然后使用shell运行查询文件。例如,要在impala-host上运行query.sql,你可以使用命令。
impala-shell.sh -i impala-host -f query.sql
下面的例子和结果是假设您已将样本数据如上所述加载到表格中。
样例:检查表格的内容
让我们先验证一下表是否包含了我们期望的数据。因为Impala经常处理包含数百万或数十亿行的表,所以在检查未知大小的表时,要包含LIMIT子句,以避免大量不必要的输出,就像最后的查询一样。如果你的交互式查询开始显示意外的数据量,在impala-shell中按Ctrl-C取消查询)。
SELECT * FROM tab1;
SELECT * FROM tab2;
SELECT * FROM tab2 LIMIT 5;
Results:
+----+-------+------------+-------------------------------+
| id | col_1 | col_2 | col_3 |
+----+-------+------------+-------------------------------+
| 1 | true | 123.123 | 2012-10-24 08:55:00 |
| 2 | false | 1243.5 | 2012-10-25 13:40:00 |
| 3 | false | 24453.325 | 2008-08-22 09:33:21.123000000 |
| 4 | false | 243423.325 | 2007-05-12 22:32:21.334540000 |
| 5 | true | 243.325 | 1953-04-22 09:11:33 |
+----+-------+------------+-------------------------------+
+----+-------+---------------+
| id | col_1 | col_2 |
+----+-------+---------------+
| 1 | true | 12789.123 |
| 2 | false | 1243.5 |
| 3 | false | 24453.325 |
| 4 | false | 2423.3254 |
| 5 | true | 243.325 |
| 60 | false | 243565423.325 |
| 70 | true | 243.325 |
| 80 | false | 243423.325 |
| 90 | true | 243.325 |
+----+-------+---------------+
+----+-------+-----------+
| id | col_1 | col_2 |
+----+-------+-----------+
| 1 | true | 12789.123 |
| 2 | false | 1243.5 |
| 3 | false | 24453.325 |
| 4 | false | 2423.3254 |
| 5 | true | 243.325 |
+----+-------+-----------+
例子: 汇总和加入
SELECT tab1.col_1, MAX(tab2.col_2), MIN(tab2.col_2)
FROM tab2 JOIN tab1 USING (id)
GROUP BY col_1 ORDER BY 1 LIMIT 5;
Results:
+-------+-----------------+-----------------+
| col_1 | max(tab2.col_2) | min(tab2.col_2) |
+-------+-----------------+-----------------+
| false | 24453.325 | 1243.5 |
| true | 12789.123 | 243.325 |
+-------+-----------------+-----------------+
例子: 子查询、聚合和连接
SELECT tab2.*
FROM tab2,
(SELECT tab1.col_1, MAX(tab2.col_2) AS max_col2
FROM tab2, tab1
WHERE tab1.id = tab2.id
GROUP BY col_1) subquery1
WHERE subquery1.max_col2 = tab2.col_2;
Results:
+----+-------+-----------+
| id | col_1 | col_2 |
+----+-------+-----------+
| 1 | true | 12789.123 |
| 3 | false | 24453.325 |
+----+-------+-----------+
例子: INSERT查询
INSERT OVERWRITE TABLE tab3
SELECT id, col_1, col_2, MONTH(col_3), DAYOFMONTH(col_3)
FROM tab1 WHERE YEAR(col_3) = 2012;
Query TAB3
to check the result:
SELECT * FROM tab3;
Results:
+----+-------+---------+-------+-----+
| id | col_1 | col_2 | month | day |
+----+-------+---------+-------+-----+
| 1 | true | 123.123 | 10 | 24 |
| 2 | false | 1243.5 | 10 | 25 |
+----+-------+---------+-------+-----+
高级教程
这些教程将引导您了解高级场景或专业功能。
将外部分区表附加到HDFS目录结构上
本教程展示了如何在HDFS中设置一个目录树,将数据文件放入最底层的子目录中,然后使用Impala外部表来查询数据文件的原始位置。
本教程使用了一个表,表里有web日志数据,年、月、日、主机都有单独的子目录。为了简单起见,我们使用少量的CSV数据,将相同的数据加载到每个分区中。
首先,我们为CSV数据做一个Impala分区表,并查看底层的HDFS目录结构,了解目录结构,以便在HDFS的其他地方重新创建。其中field1、field2、field3这三列对应的是CSV数据文件的内容。年、月、日、主机列都是以表结构中的子目录来表示,并不是CSV文件的一部分。我们对每一列都使用STRING,这样就可以产生一致的子目录名,前导零的长度一致。
create database external_partitions; use external_partitions; create table logs (field1 string, field2 string, field3 string) partitioned by (year string, month string , day string, host string) row format delimited fields terminated by ','; insert into logs partition (year="2013", month="07", day="28", host="host1") values ("foo","foo","foo"); insert into logs partition (year="2013", month="07", day="28", host="host2") values ("foo","foo","foo"); insert into logs partition (year="2013", month="07", day="29", host="host1") values ("foo","foo","foo"); insert into logs partition (year="2013", month="07", day="29", host="host2") values ("foo","foo","foo"); insert into logs partition (year="2013", month="08", day="01", host="host1") values ("foo","foo","foo");
回到Linux shell中,我们检查HDFS目录结构。(你的Impala数据目录可能在不同的位置,由于历史原因,它有时在HDFS路径/user/hive/warehouse下)。我们使用hdfs dfs -ls命令检查每个分区列对应的嵌套子目录,每一级都有单独的子目录(名称中带=),代表每个分区列的不同值。当我们到了最底层的子目录时,使用hdfs dfs -cat命令检查数据文件,可以看到Impala中INSERT语句产生的CSV格式的数据。
$ hdfs dfs -ls /user/impala/warehouse/external_partitions.db
Found 1 items
drwxrwxrwt - impala hive 0 2013-08-07 12:24 /user/impala/warehouse/external_partitions.db/logs
$ hdfs dfs -ls /user/impala/warehouse/external_partitions.db/logs
Found 1 items
drwxr-xr-x - impala hive 0 2013-08-07 12:24 /user/impala/warehouse/external_partitions.db/logs/year=2013
$ hdfs dfs -ls /user/impala/warehouse/external_partitions.db/logs/year=2013
Found 2 items
drwxr-xr-x - impala hive 0 2013-08-07 12:23 /user/impala/warehouse/external_partitions.db/logs/year=2013/month=07
drwxr-xr-x - impala hive 0 2013-08-07 12:24 /user/impala/warehouse/external_partitions.db/logs/year=2013/month=08
$ hdfs dfs -ls /user/impala/warehouse/external_partitions.db/logs/year=2013/month=07
Found 2 items
drwxr-xr-x - impala hive 0 2013-08-07 12:22 /user/impala/warehouse/external_partitions.db/logs/year=2013/month=07/day=28
drwxr-xr-x - impala hive 0 2013-08-07 12:23 /user/impala/warehouse/external_partitions.db/logs/year=2013/month=07/day=29
$ hdfs dfs -ls /user/impala/warehouse/external_partitions.db/logs/year=2013/month=07/day=28
Found 2 items
drwxr-xr-x - impala hive 0 2013-08-07 12:21 /user/impala/warehouse/external_partitions.db/logs/year=2013/month=07/day=28/host=host1
drwxr-xr-x - impala hive 0 2013-08-07 12:22 /user/impala/warehouse/external_partitions.db/logs/year=2013/month=07/day=28/host=host2
$ hdfs dfs -ls /user/impala/warehouse/external_partitions.db/logs/year=2013/month=07/day=28/host=host1
Found 1 items
-rw-r--r-- 3 impala hive 12 2013-08-07 12:21 /user/impala/warehouse/external_partiti
ons.db/logs/year=2013/month=07/day=28/host=host1/3981726974111751120--8907184999369517436_822630111_data.0
$ hdfs dfs -cat /user/impala/warehouse/external_partitions.db/logs/year=2013/month=07/day=28/
host=host1/3981726974111751120--8 907184999369517436_822630111_data.0
foo,foo,foo
还是在Linux shell中,我们使用hdfs dfs -mkdir在Impala控制的HDFS目录树外创建几个数据目录(本例中为/user/impala/warehouse,在你的情况下可能有所不同)。根据你的配置,你可能需要以有权限的用户登录,才能写入这个HDFS目录树;例如,这里显示的命令是在以hdfs用户登录时运行的。
$ hdfs dfs -mkdir -p /user/impala/data/logs/year=2013/month=07/day=28/host=host1
$ hdfs dfs -mkdir -p /user/impala/data/logs/year=2013/month=07/day=28/host=host2
$ hdfs dfs -mkdir -p /user/impala/data/logs/year=2013/month=07/day=28/host=host1
$ hdfs dfs -mkdir -p /user/impala/data/logs/year=2013/month=07/day=29/host=host1
$ hdfs dfs -mkdir -p /user/impala/data/logs/year=2013/month=08/day=01/host=host1
我们制作一个微小的CSV文件,其值与前面使用的INSERT语句中的值不同,并在我们将用作Impala分区的每个子目录内放一份副本。
$ cat >dummy_log_data
bar,baz,bletch
$ hdfs dfs -mkdir -p /user/impala/data/external_partitions/year=2013/month=08/day=01/host=host1
$ hdfs dfs -mkdir -p /user/impala/data/external_partitions/year=2013/month=07/day=28/host=host1
$ hdfs dfs -mkdir -p /user/impala/data/external_partitions/year=2013/month=07/day=28/host=host2
$ hdfs dfs -mkdir -p /user/impala/data/external_partitions/year=2013/month=07/day=29/host=host1
$ hdfs dfs -put dummy_log_data /user/impala/data/logs/year=2013/month=07/day=28/host=host1
$ hdfs dfs -put dummy_log_data /user/impala/data/logs/year=2013/month=07/day=28/host=host2
$ hdfs dfs -put dummy_log_data /user/impala/data/logs/year=2013/month=07/day=29/host=host1
$ hdfs dfs -put dummy_log_data /user/impala/data/logs/year=2013/month=08/day=01/host=host1
回到impala-shell解释器中,我们将原来的Impala管理的表移到一边,并创建一个新的外部表,表的LOCATION子句指向我们设置了所有分区子目录和数据文件的目录。
use external_partitions;
alter table logs rename to logs_original;
create external table logs (field1 string, field2 string, field3 string)
partitioned by (year string, month string, day string, host string)
row format delimited fields terminated by ','
location '/user/impala/data/logs';
由于分区子目录和数据文件在数据生命周期中来来去去,因此必须在Impala识别它们所包含的数据文件之前,通过ALTER TABLE语句识别每个分区。
alter table logs add partition (year="2013",month="07",day="28",host="host1")
alter table log_type add partition (year="2013",month="07",day="28",host="host2");
alter table log_type add partition (year="2013",month="07",day="29",host="host1");
alter table log_type add partition (year="2013",month="08",day="01",host="host1");
我们为表发出REFRESH语句,当数据文件被手动添加、删除或更改时,这总是一个安全的做法。然后数据就可以被查询了。SELECT *语句说明,我们琐碎的CSV文件中的数据在我们复制它的每个分区中都被识别。虽然在这种情况下只有几行,但我们在这个测试查询中包含了一个limit子句,以防数据比我们预期的多。
refresh log_type;
select * from log_type limit 100;
+--------+--------+--------+------+-------+-----+-------+
| field1 | field2 | field3 | year | month | day | host |
+--------+--------+--------+------+-------+-----+-------+
| bar | baz | bletch | 2013 | 07 | 28 | host1 |
| bar | baz | bletch | 2013 | 08 | 01 | host1 |
| bar | baz | bletch | 2013 | 07 | 29 | host1 |
| bar | baz | bletch | 2013 | 07 | 28 | host2 |
+--------+--------+--------+------+-------+-----+-------+
在Impala和Hive之间来回切换
有时,你可能会发现切换到Hive shell来执行一些数据加载或转换操作很方便,特别是对诸如RCFile、SequenceFile和Avro这样的文件格式,目前Impala可以查询但不能写入。
每当你通过Hive创建、放弃或改变一个表或其他类型的对象时,下一次切换回impala-shell解释器时,都要一次性发出INVALIDATE METADATA语句,以便Impala识别新的或改变的对象。
每当你通过Hive(甚至通过手动HDFS操作,如hdfs命令)加载、插入或更改现有表中的数据时,下一次切换回impala-shell解释器时,都要发出一次性REFRESH table_name语句,以便Impala识别新的或更改的数据。
要想了解这个过程是如何为REFRESH语句工作的,请看在Impala中创建RCFile和SequenceFile表,通过Hive加载数据,然后通过Impala查询数据的例子。有关这些示例,请参见使用RCFile文件格式与Impala表和使用SequenceFile文件格式与Impala表。
有关显示此过程如何适用于 INVALIDATE METADATA 语句的例子,请看在 Hive 中创建和加载 Avro 表,然后通过 Impala 查询数据的例子。有关该示例,请参见使用Avro文件格式与Impala表。
请注意,Impala不支持Avro表。
最初,Impala并不支持UDF,但从Impala 1.2开始,Impala中就有了这个功能。原来通过Hive进行的一些INSERT ... SELECT转换,现在可以通过Impala完成。详情请参见用户定义函数(UDFs)。
在Impala 1.2之前,需要在你连接并发出查询的每个Impala节点上发出REFRESH和INVALIDATE METADATA语句。在Impala 1.2和更高版本中,当你在任何一个Impala节点上发出这两条语句中的任何一条时,结果会被广播到集群中的所有Impala节点上,使得在Hive中每一轮DDL或ETL操作后,真正做到一步到位。
交叉连接和笛卡儿产品与CROSS JOIN操作器一起使用
最初,Impala限制了连接查询,因此它们必须在连接操作符两边的表列之间至少包含一个平等比较。由于Impala通常会处理大量的表,任何产生完整笛卡尔乘积作为结果集的错误编码查询都会消耗大量的集群资源。
在Impala 1.2.2和更高版本中,当你在查询中使用CROSS JOIN操作符时,这个限制被取消了。你仍然不能从一个查询中删除所有的WHERE子句,比如SELECT * FROM t1 JOIN t2,以产生两个表的所有行组合。但是你可以使用CROSS JOIN操作符来显式地请求这样的笛卡尔积。通常情况下,这种操作适用于较小的表,其结果集仍然适合在单个Impala节点的内存中。
下面的例子设置了用于一系列漫画书的数据,其中的角色互相战斗。一开始,我们使用等价连接查询,只允许来自同一时间段和同一星球的角色相遇。
[localhost:21000] > create table heroes (name string, era string, planet string);
[localhost:21000] > create table villains (name string, era string, planet string);
[localhost:21000] > insert into heroes values
> ('Tesla','20th century','Earth'),
> ('Pythagoras','Antiquity','Earth'),
> ('Zopzar','Far Future','Mars');
Inserted 3 rows in 2.28s
[localhost:21000] > insert into villains values
> ('Caligula','Antiquity','Earth'),
> ('John Dillinger','20th century','Earth'),
> ('Xibulor','Far Future','Venus');
Inserted 3 rows in 1.93s
[localhost:21000] > select concat(heroes.name,' vs. ',villains.name) as battle
> from heroes join villains
> where heroes.era = villains.era and heroes.planet = villains.planet;
+--------------------------+
| battle |
+--------------------------+
| Tesla vs. John Dillinger |
| Pythagoras vs. Caligula |
+--------------------------+
Returned 2 row(s) in 0.47s
读者要求更多的动作,所以我们增加了时间旅行和空间旅行的元素,这样任何英雄都可以面对任何反派。在Impala 1.2.2之前,这种类型的查询是不可能的,因为所有的连接都必须引用两个表之间的匹配值。
[localhost:21000] > -- Cartesian product not possible in Impala 1.1.
> select concat(heroes.name,' vs. ',villains.name) as battle from heroes join villains;
ERROR: NotImplementedException: Join between 'heroes' and 'villains' requires at least one conjunctive equality predicate between the two tables
在Impala 1.2.2中,我们稍微重写了一下查询,使用CROSS JOIN而不是JOIN,现在结果集包括所有组合。
[localhost:21000] > -- Cartesian product available in Impala 1.2.2 with the CROSS JOIN syntax.
> select concat(heroes.name,' vs. ',villains.name) as battle from heroes cross join villains;
+-------------------------------+
| battle |
+-------------------------------+
| Tesla vs. Caligula |
| Tesla vs. John Dillinger |
| Tesla vs. Xibulor |
| Pythagoras vs. Caligula |
| Pythagoras vs. John Dillinger |
| Pythagoras vs. Xibulor |
| Zopzar vs. Caligula |
| Zopzar vs. John Dillinger |
| Zopzar vs. Xibulor |
+-------------------------------+
Returned 9 row(s) in 0.33s
两个表的行的全部组合称为笛卡尔积。这种类型的结果集通常用于创建网格数据结构。你也可以通过包含WHERE子句来过滤结果集,WHERE子句并不明确地比较两个表之间的列。下面的例子显示了如何生成一个用于图表的年份和季度组合的列表,然后再生成一个只包含所选季度的较短列表。
[localhost:21000] > create table x_axis (x int);
[localhost:21000] > create table y_axis (y int);
[localhost:21000] > insert into x_axis values (1),(2),(3),(4);
Inserted 4 rows in 2.14s
[localhost:21000] > insert into y_axis values (2010),(2011),(2012),(2013),(2014);
Inserted 5 rows in 1.32s
[localhost:21000] > select y as year, x as quarter from x_axis cross join y_axis;
+------+---------+
| year | quarter |
+------+---------+
| 2010 | 1 |
| 2011 | 1 |
| 2012 | 1 |
| 2013 | 1 |
| 2014 | 1 |
| 2010 | 2 |
| 2011 | 2 |
| 2012 | 2 |
| 2013 | 2 |
| 2014 | 2 |
| 2010 | 3 |
| 2011 | 3 |
| 2012 | 3 |
| 2013 | 3 |
| 2014 | 3 |
| 2010 | 4 |
| 2011 | 4 |
| 2012 | 4 |
| 2013 | 4 |
| 2014 | 4 |
+------+---------+
Returned 20 row(s) in 0.38s
[localhost:21000] > select y as year, x as quarter from x_axis cross join y_axis where x in (1,3);
+------+---------+
| year | quarter |
+------+---------+
| 2010 | 1 |
| 2011 | 1 |
| 2012 | 1 |
| 2013 | 1 |
| 2014 | 1 |
| 2010 | 3 |
| 2011 | 3 |
| 2012 | 3 |
| 2013 | 3 |
| 2014 | 3 |
+------+---------+
Returned 10 row(s) in 0.39s
处理未知模式的Parquet文件
随着数据管道开始包含更多的方面,如NoSQL或松散指定的模式,你可能会遇到这样的情况:你有数据文件(特别是Parquet格式),你不知道精确的表定义。本教程展示了如何围绕来自非Impala甚至非SQL源的数据构建Impala表,在这种情况下,你无法控制表的布局,可能不熟悉数据的特性。
本教程中使用的数据代表了1987年10月到2008年4月的航空公司准点到达统计数据。详细内容请看2009年ASA数据博览网站。你也可以看到各列的解释;为了本练习的目的,等到跟随教程之后再检查模式,以便更好地模拟现实生活中不能依赖对数据值的范围和表示的假设和断言的情况。
下载数据文件到HDFS
首先,我们下载并解压数据文件。共有8个文件,共计1.4GB。
$ wget -O airlines_parquet.tar.gz https://home.apache.org/~arodoni/airlines_parquet.tar.gz
$ wget https://home.apache.org/~arodoni/airlines_parquet.tar.gz.sha512
$ shasum -a 512 -c airlines_parquet.tar.gz.sha512
airlines_parquet.tar.gz: OK
$ tar xvzf airlines_parquet.tar.gz
$ cd airlines_parquet/
$ du -kch *.parq
253M 4345e5eef217aa1b-c8f16177f35fd983_1150363067_data.0.parq
14M 4345e5eef217aa1b-c8f16177f35fd983_1150363067_data.1.parq
253M 4345e5eef217aa1b-c8f16177f35fd984_501176748_data.0.parq
64M 4345e5eef217aa1b-c8f16177f35fd984_501176748_data.1.parq
184M 4345e5eef217aa1b-c8f16177f35fd985_1199995767_data.0.parq
241M 4345e5eef217aa1b-c8f16177f35fd986_2086627597_data.0.parq
212M 4345e5eef217aa1b-c8f16177f35fd987_1048668565_data.0.parq
152M 4345e5eef217aa1b-c8f16177f35fd988_1432111844_data.0.parq
1.4G total
接下来,我们把Parquet数据文件放到HDFS中,全部集中在一个目录下,目录和文件都有权限,这样impala用户就可以读取。
解压后,我们看到最大的Parquet文件是253MB。当把Parquet文件复制到HDFS中供Impala使用时,为了获得最大的查询性能,要确保每个文件都驻留在一个HDFS数据块中。因此,我们在hdfs dfs -put命令上使用参数-Ddfs.block.size=253m,选取一个大于任何单个文件的大小,并指定其为块大小。
$ sudo -u hdfs hdfs dfs -mkdir -p /user/impala/staging/airlines
$ sudo -u hdfs hdfs dfs -Ddfs.block.size=253m -put *.parq /user/impala/staging/airlines
$ sudo -u hdfs hdfs dfs -ls /user/impala/staging
Found 1 items
$ sudo -u hdfs hdfs dfs -ls /user/impala/staging/airlines
Found 8 items
创建数据库和表
随着文件在HDFS中的可访问位置,你可以创建一个数据库表,使用这些文件中的数据。
- CREATE EXTERNAL语法和LOCATION属性将Impala指向适当的HDFS目录。
- LIKE PARQUET'path_to_any_parquet_file'子句意味着我们跳过列名和类型的列表;Impala会自动从数据文件中直接获取列名和数据类型。目前,这种技术只对Parquet文件有效)。
- 忽略关于HDFS中缺乏对文件的READ_WRITE访问权限的警告;impala用户可以读取文件,这将足以让我们实验查询,并执行一些复制和转换操作到其他表中。
$ impala-shell
> CREATE DATABASE airlines_data;
USE airlines_data;
CREATE EXTERNAL TABLE airlines_external
LIKE PARQUET 'hdfs:staging/airlines/4345e5eef217aa1b-c8f16177f35fd983_1150363067_data.0.parq'
STORED AS PARQUET LOCATION 'hdfs:staging/airlines';
WARNINGS: Impala does not have READ_WRITE access to path 'hdfs://myhost.com:8020/user/impala/staging'
检查物理和逻辑模式
- 创建了表之后,我们要检查它的物理和逻辑特性,以确认数据确实存在,并且是以我们可以处理的格式和形状存在。
- SHOW TABLE STATS语句给出了表的一个非常高层次的总结,显示了表有多少个文件,包含多少总数据。同时,它还确认了该表希望所有相关的数据文件都是Parquet格式的。(能够处理各种不同格式的HDFS数据文件,意味着数据文件的格式,和表期望数据文件的格式有可能不匹配)。
- SHOW FILES语句确认表中的数据具有预期的数量、名称和原始Parquet文件的大小。
- DESCRIBE语句(或其缩写DESC)确认了Impala从Parquet文件中读取该元数据后自动创建的列的名称和类型。
- DESCRIBE FORMATTED 语句在列定义的同时打印出一些额外的细节。在这个练习中,我们关心的部分是。
- 表的包含数据库
- HDFS中相关数据文件的位置。
- 该表是一个外部表,所以当我们完成实验并放弃该表时,Impala不会删除HDFS文件。
- 该表的设置是为了专门处理Parquet格式的文件。
> SHOW TABLE STATS airlines_external;
+-------+--------+--------+--------------+-------------------+---------+-------------------+
| #Rows | #Files | Size | Bytes Cached | Cache Replication | Format | Incremental stats |
+-------+--------+--------+--------------+-------------------+---------+-------------------+
| -1 | 8 | 1.34GB | NOT CACHED | NOT CACHED | PARQUET | false |
+-------+--------+--------+--------------+-------------------+---------+-------------------+
> SHOW FILES IN airlines_external;
+----------------------------------------------------------------------------------------+----------+-----------+
| path | size | partition |
+----------------------------------------------------------------------------------------+----------+-----------+
| /user/impala/staging/airlines/4345e5eef217aa1b-c8f16177f35fd983_1150363067_data.0.parq | 252.99MB | |
| /user/impala/staging/airlines/4345e5eef217aa1b-c8f16177f35fd983_1150363067_data.1.parq | 13.43MB | |
| /user/impala/staging/airlines/4345e5eef217aa1b-c8f16177f35fd984_501176748_data.0.parq | 252.84MB | |
| /user/impala/staging/airlines/4345e5eef217aa1b-c8f16177f35fd984_501176748_data.1.parq | 63.92MB | |
| /user/impala/staging/airlines/4345e5eef217aa1b-c8f16177f35fd985_1199995767_data.0.parq | 183.64MB | |
| /user/impala/staging/airlines/4345e5eef217aa1b-c8f16177f35fd986_2086627597_data.0.parq | 240.04MB | |
| /user/impala/staging/airlines/4345e5eef217aa1b-c8f16177f35fd987_1048668565_data.0.parq | 211.35MB | |
| /user/impala/staging/airlines/4345e5eef217aa1b-c8f16177f35fd988_1432111844_data.0.parq | 151.46MB | |
+----------------------------------------------------------------------------------------+----------+-----------+
> DESCRIBE airlines_external;
+---------------------+--------+---------------------------------------------------+
| name | type | comment |
+---------------------+--------+---------------------------------------------------+
| year | int | Inferred from Parquet file. |
| month | int | Inferred from Parquet file. |
| day | int | Inferred from Parquet file. |
| dayofweek | int | Inferred from Parquet file. |
| dep_time | int | Inferred from Parquet file. |
| crs_dep_time | int | Inferred from Parquet file. |
| arr_time | int | Inferred from Parquet file. |
| crs_arr_time | int | Inferred from Parquet file. |
| carrier | string | Inferred from Parquet file. |
| flight_num | int | Inferred from Parquet file. |
| tail_num | int | Inferred from Parquet file. |
| actual_elapsed_time | int | Inferred from Parquet file. |
| crs_elapsed_time | int | Inferred from Parquet file. |
| airtime | int | Inferred from Parquet file. |
| arrdelay | int | Inferred from Parquet file. |
| depdelay | int | Inferred from Parquet file. |
| origin | string | Inferred from Parquet file. |
| dest | string | Inferred from Parquet file. |
| distance | int | Inferred from Parquet file. |
| taxi_in | int | Inferred from Parquet file. |
| taxi_out | int | Inferred from Parquet file. |
| cancelled | int | Inferred from Parquet file. |
| cancellation_code | string | Inferred from Parquet file. |
| diverted | int | Inferred from Parquet file. |
| carrier_delay | int | Inferred from Parquet file. |
| weather_delay | int | Inferred from Parquet file. |
| nas_delay | int | Inferred from Parquet file. |
| security_delay | int | Inferred from Parquet file. |
| late_aircraft_delay | int | Inferred from Parquet file. |
+---------------------+--------+---------------------------------------------------+
> DESCRIBE FORMATTED airlines_external;
+------------------------------+-------------------------------
| name | type
+------------------------------+-------------------------------
...
| # Detailed Table Information | NULL
| Database: | airlines_data
| Owner: | impala
...
| Location: | /user/impala/staging/airlines
| Table Type: | EXTERNAL_TABLE
...
| # Storage Information | NULL
| SerDe Library: | org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe
| InputFormat: | org.apache.hadoop.hive.ql.io.parquet.MapredParquetInputForma
| OutputFormat: | org.apache.hadoop.hive.ql.io.parquet.MapredParquetOutputFormat
...
数据分析
现在我们确信Impala表和底层Parquet文件之间的连接是稳固的,我们运行一些初始查询来了解数据的特性:总体行数,以及某些列中的范围和多少不同的值。
> SELECT COUNT(*) FROM airlines_external;
+-----------+
| count(*) |
+-----------+
| 123534969 |
+-----------+
NDV()函数返回若干个不同的值,出于性能考虑,当列中有很多不同的值时,这是一个估计值,但当cardinality小于16K时,这个估计值是精确的,使用NDV()函数进行这种探索,而不是COUNT(DISTINCT colname),因为Impala可以在一个查询中评估多个NDV()函数,但只能评估COUNT DISTINCT的一个实例。
> SElECT NDV(carrier), NDV(flight_num), NDV(tail_num),
NDV(origin), NDV(dest) FROM airlines_external;
+--------------+-----------------+---------------+-------------+-----------+
| ndv(carrier) | ndv(flight_num) | ndv(tail_num) | ndv(origin) | ndv(dest) |
+--------------+-----------------+---------------+-------------+-----------+
| 29 | 8463 | 3 | 342 | 349 |
+--------------+-----------------+---------------+-------------+-----------+
> SELECT tail_num, COUNT(*) AS howmany FROM airlines_external
GROUP BY tail_num;
+----------+-----------+
| tail_num | howmany |
+----------+-----------+
| NULL | 123122001 |
| 715 | 1 |
| 0 | 406405 |
| 112 | 6562 |
+----------+-----------+
> SELECT DISTINCT dest FROM airlines_external
WHERE dest NOT IN (SELECT origin FROM airlines_external);
+------+
| dest |
+------+
| CBM |
| SKA |
| LAR |
| RCA |
| LBF |
+------+
> SELECT DISTINCT dest FROM airlines_external
WHERE dest NOT IN (SELECT DISTINCT origin FROM airlines_external);
+------+
| dest |
+------+
| CBM |
| SKA |
| LAR |
| RCA |
| LBF |
+------+
> SELECT DISTINCT origin FROM airlines_external
WHERE origin NOT IN (SELECT DISTINCT dest FROM airlines_external);
Fetched 0 row(s) in 2.63
通过上面的查询,我们看到不同的航空公司、航班号、出发地机场和目的地机场的数量都不多。从这个查询中跳出了两件事:尾数_num值的数量比我们预期的要少得多,而且目的地机场比始发地机场多。让我们进一步挖掘。我们发现,大多数 tail_num 值都是 NULL。看来这是一个实验性的列,没有准确填写。我们记下,如果我们以这个数据为出发点,我们就忽略这一列。我们还发现,某些机场在ORIGIN列中表示,但在DEST列中没有表示;现在我们知道,我们不能依赖这些机场代码集是相同的假设。
注:第一个SELECT DISTINCT DEST查询耗时近40秒。我们希望在这样一个小于2GB的小数据集上进行的所有查询最多只需要几秒钟。原因在于表达式NOT IN (SELECT origin FROM airlines_external)会产生一个1.23亿行的中间结果集,然后在每个数据节点上针对目的地机场的小数据集运行1.23亿次比较。NOT IN运算符在内部的工作方式意味着这个有1.23亿行的中间结果集可能会通过网络传输到集群中的每个数据节点上。在NOT IN子查询内部应用另一个DISTINCT意味着中间结果集只有340项,从而大大减少了网络流量和比较操作。添加了DISTINCT的更高效的查询速度大约是7倍。
接下来,我们尝试做一个简单的计算,结果按年份划分。这就会发现,有些年份的通话时间列中没有数据。这意味着我们也许可以在涉及某些日期范围的查询中使用该列,但我们不能指望它总是可靠的。在对一个数据集进行初步探索时,一个列是否包含任何NULL值,如果包含,它们的数量、比例和分布情况如何,这个问题会反复出现。
> SELECT year, SUM(airtime) FROM airlines_external
GROUP BY year ORDER BY year DESC;
+------+--------------+
| year | sum(airtime) |
+------+--------------+
| 2008 | 713050445 |
| 2007 | 748015545 |
| 2006 | 720372850 |
| 2005 | 708204026 |
| 2004 | 714276973 |
| 2003 | 665706940 |
| 2002 | 549761849 |
| 2001 | 590867745 |
| 2000 | 583537683 |
| 1999 | 561219227 |
| 1998 | 538050663 |
| 1997 | 536991229 |
| 1996 | 519440044 |
| 1995 | 513364265 |
| 1994 | NULL |
| 1993 | NULL |
| 1992 | NULL |
| 1991 | NULL |
| 1990 | NULL |
| 1989 | NULL |
| 1988 | NULL |
| 1987 | NULL |
+------+--------------+
有了NULL值的概念,我们再来看看我们发现有很多NULL的尾数_num列。为了更好的理解,我们来量化一下该列中的NULL值与非NULL值。首先,我们只是统计整体的行数与该列中的非NULL值。这个初始结果给人的感觉是非NULL值相对较少,但我们可以在一次查询中更清晰地分解。一旦我们有了COUNT(*)和COUNT(colname)的数字,我们就可以将这个初始查询编码在一个WITH子句中,然后运行一个后续查询,对这些值执行多次算术运算。看到所有行中只有三分之一的尾数_num列有非NULL值,清楚地说明了该列的作用不大。
> SELECT COUNT(*) AS 'rows', COUNT(tail_num) AS 'non-null tail numbers'
FROM airlines_external;
+-----------+-----------------------+
| rows | non-null tail numbers |
+-----------+-----------------------+
| 123534969 | 412968 |
+-----------+-----------------------+
> WITH t1 AS
(SELECT COUNT(*) AS 'rows', COUNT(tail_num) AS 'nonnull'
FROM airlines_external)
SELECT `rows`, `nonnull`, `rows` - `nonnull` AS 'nulls',
(`nonnull` / `rows`) * 100 AS 'percentage non-null'
FROM t1;
+-----------+---------+-----------+---------------------+
| rows | nonnull | nulls | percentage non-null |
+-----------+---------+-----------+---------------------+
| 123534969 | 412968 | 123122001 | 0.3342923897119365 |
+-----------+---------+-----------+---------------------+
通过使用这些技术对其他列进行检查,我们可以对数据在整个表中的分布方式形成一个心理图景,以及哪些列对查询目的来说是最重要的。在本教程中,我们主要关注可能持有离散值的字段,而不是像 actual_elapsed_time 这样的列,它们的名字表明它们持有测量值。一旦我们清楚地了解到哪些问题值得询问,以及我们可能会寻找什么样的趋势,我们就会对这些列进行更深入的研究。对于初步探索的最后一块,让我们看看年份列。通过简单的GROUP BY查询可以看出,它有一个明确的范围,不同值的数量可以控制,而且不同年份的行数分布相对均匀。
> SELECT MIN(year), MAX(year), NDV(year) FROM airlines_external;
+-----------+-----------+-----------+
| min(year) | max(year) | ndv(year) |
+-----------+-----------+-----------+
| 1987 | 2008 | 22 |
+-----------+-----------+-----------+
> SELECT year, COUNT(*) howmany FROM airlines_external
GROUP BY year ORDER BY year DESC;
+------+---------+
| year | howmany |
+------+---------+
| 2008 | 7009728 |
| 2007 | 7453215 |
| 2006 | 7141922 |
| 2005 | 7140596 |
| 2004 | 7129270 |
| 2003 | 6488540 |
| 2002 | 5271359 |
| 2001 | 5967780 |
| 2000 | 5683047 |
| 1999 | 5527884 |
| 1998 | 5384721 |
| 1997 | 5411843 |
| 1996 | 5351983 |
| 1995 | 5327435 |
| 1994 | 5180048 |
| 1993 | 5070501 |
| 1992 | 5092157 |
| 1991 | 5076925 |
| 1990 | 5270893 |
| 1989 | 5041200 |
| 1988 | 5202096 |
| 1987 | 1311826 |
+------+---------+
我们可以对这种最初的原始格式的数据进行深入研究,就像我们从网上下载的那样。如果事实证明这个数据集是有用的,值得在Impala中持久化,以便进行大量的查询,我们可能会想把它复制到一个内部的表中,让Impala管理数据文件,也许会重新组织一下,以提高工作效率。在本教程的下一阶段,我们将原始数据复制到一个分区表中,仍然是Parquet格式。基于年份列的分区让我们可以用WHERE year = 2001或者WHERE year BETWEEN 1989 AND 1999这样的子句来运行查询,通过忽略所需范围之外的所有年份的数据,可以极大地减少I/O。Impala不需要读取所有数据,然后决定哪些行属于匹配的年份,而是可以只对特定年份分区的数据文件进行归零。为此,Impala会对数据文件进行物理重组,将每个年份的行放入每个年份值的单独HDFS目录中的数据文件中。在这一过程中,我们还将摆脱被证明几乎完全是NULL的tail_num列。
第一步是创建一个新表,其布局与原来的airlines_external表非常相似。我们将通过对第一个表进行逆向工程的CREATE TABLE语句来实现,然后稍作调整,为年份加入一个PARTITION BY子句,并排除tail_num列。SHOW CREATE TABLE语句为我们提供了一个起点。
虽然我们可以将该输出结果编辑成一条新的SQL语句,但是所有的ASCII框字符使得这样的编辑工作很不方便。为了得到一个更简练的CREATE TABLE,我们使用-B选项重新启动impala-shell命令,它关闭了方块绘制行为。
$ impala-shell -i localhost -B -d airlines_data;
> SHOW CREATE TABLE airlines_external;
"CREATE EXTERNAL TABLE airlines_data.airlines_external (
year INT COMMENT 'inferred from: optional int32 year',
month INT COMMENT 'inferred from: optional int32 month',
day INT COMMENT 'inferred from: optional int32 day',
dayofweek INT COMMENT 'inferred from: optional int32 dayofweek',
dep_time INT COMMENT 'inferred from: optional int32 dep_time',
crs_dep_time INT COMMENT 'inferred from: optional int32 crs_dep_time',
arr_time INT COMMENT 'inferred from: optional int32 arr_time',
crs_arr_time INT COMMENT 'inferred from: optional int32 crs_arr_time',
carrier STRING COMMENT 'inferred from: optional binary carrier',
flight_num INT COMMENT 'inferred from: optional int32 flight_num',
tail_num INT COMMENT 'inferred from: optional int32 tail_num',
actual_elapsed_time INT COMMENT 'inferred from: optional int32 actual_elapsed_time',
crs_elapsed_time INT COMMENT 'inferred from: optional int32 crs_elapsed_time',
airtime INT COMMENT 'inferred from: optional int32 airtime',
arrdelay INT COMMENT 'inferred from: optional int32 arrdelay',
depdelay INT COMMENT 'inferred from: optional int32 depdelay',
origin STRING COMMENT 'inferred from: optional binary origin',
dest STRING COMMENT 'inferred from: optional binary dest',
distance INT COMMENT 'inferred from: optional int32 distance',
taxi_in INT COMMENT 'inferred from: optional int32 taxi_in',
taxi_out INT COMMENT 'inferred from: optional int32 taxi_out',
cancelled INT COMMENT 'inferred from: optional int32 cancelled',
cancellation_code STRING COMMENT 'inferred from: optional binary cancellation_code',
diverted INT COMMENT 'inferred from: optional int32 diverted',
carrier_delay INT COMMENT 'inferred from: optional int32 carrier_delay',
weather_delay INT COMMENT 'inferred from: optional int32 weather_delay',
nas_delay INT COMMENT 'inferred from: optional int32 nas_delay',
security_delay INT COMMENT 'inferred from: optional int32 security_delay',
late_aircraft_delay INT COMMENT 'inferred from: optional int32 late_aircraft_delay'
)
STORED AS PARQUET
LOCATION 'hdfs://a1730.example.com:8020/user/impala/staging/airlines'
TBLPROPERTIES ('numFiles'='0', 'COLUMN_STATS_ACCURATE'='false',
'transient_lastDdlTime'='1439425228', 'numRows'='-1', 'totalSize'='0',
'rawDataSize'='-1')"
将CREATE TABLE语句复制并粘贴到文本编辑器中进行微调后,我们退出并重新启动impala-shell,不使用-B选项,以切换回常规输出。
接下来,我们运行从 SHOW CREATE TABLE 语句改编而来的 CREATE TABLE 语句。我们保留了STORED AS PARQUET子句,因为我们希望在一定程度上重新排列数据,但仍然保持高性能的Parquet格式。LOCATION和TBLPROPERTIES子句与这个新表无关,所以我们编辑掉这些子句。因为我们要根据年份列对新表进行分区,所以我们将该列名(及其类型)移到一个新的PARTITIONED BY子句中。
> CREATE TABLE airlines_data.airlines
(month INT,
day INT,
dayofweek INT,
dep_time INT,
crs_dep_time INT,
arr_time INT,
crs_arr_time INT,
carrier STRING,
flight_num INT,
actual_elapsed_time INT,
crs_elapsed_time INT,
airtime INT,
arrdelay INT,
depdelay INT,
origin STRING,
dest STRING,
distance INT,
taxi_in INT,
taxi_out INT,
cancelled INT,
cancellation_code STRING,
diverted INT,
carrier_delay INT,
weather_delay INT,
nas_delay INT,
security_delay INT,
late_aircraft_delay INT)
PARTITIONED BY (year INT)
STORED AS PARQUET
;
接下来,我们用一条INSERT语句将原来表中的所有行复制到这个新表中。(我们编辑了CREATE TABLE语句,使之成为一条INSERT语句,列名的顺序也是一样的。) 唯一的改变是增加一个PARTITION(year)子句,并将年份列移到INSERT语句的SELECT列表的最后。指定PARTITION(year),而不是一个固定的值,比如PARTITION(year=2000),这意味着Impala会根据SELECT列表中最后一列的值计算出每行的分区值。这是第一个合法地花费大量时间的SQL语句,因为来自不同年份的行在集群中被洗牌;进入每个分区的行被收集在一个节点上,然后再写入一个或多个新的数据文件。
> INSERT INTO airlines_data.airlines
PARTITION (year)
SELECT
month,
day,
dayofweek,
dep_time,
crs_dep_time,
arr_time,
crs_arr_time,
carrier,
flight_num,
actual_elapsed_time,
crs_elapsed_time,
airtime,
arrdelay,
depdelay,
origin,
dest,
distance,
taxi_in,
taxi_out,
cancelled,
cancellation_code,
diverted,
carrier_delay,
weather_delay,
nas_delay,
security_delay,
late_aircraft_delay,
year
FROM airlines_data.airlines_external;
一旦分区或联接查询出现,掌握统计资料很重要,Impala可以利用这些资料来优化相应表的查询。COMPUTE INCREMENTAL STATS语句是收集分区表的统计数据的方法。然后通过SHOW TABLE STATS语句来确认每个分区的统计数据是否到位,也可以说明每个分区有多少文件和多少原始数据。
> COMPUTE INCREMENTAL STATS airlines;
+-------------------------------------------+
| summary |
+-------------------------------------------+
| Updated 22 partition(s) and 27 column(s). |
+-------------------------------------------+
> SHOW TABLE STATS airlines;
+-------+-----------+--------+----------+--------------+-------------------+---------+-------------------+----------------------------------------------------------------------------------------------------------+
| year | #Rows | #Files | Size | Bytes Cached | Cache Replication | Format | Incremental stats | Location |
+-------+-----------+--------+----------+--------------+-------------------+---------+-------------------+----------------------------------------------------------------------------------------------------------+
| 1987 | 1311826 | 1 | 11.75MB | NOT CACHED | NOT CACHED | PARQUET | true | hdfs://myhost.com:8020/user/hive/warehouse/airline_data.db/airlines/year=1987 |
| 1988 | 5202096 | 1 | 44.04MB | NOT CACHED | NOT CACHED | PARQUET | true | hdfs://myhost.com:8020/user/hive/warehouse/airline_data.db/airlines/year=1988 |
| 1989 | 5041200 | 1 | 46.07MB | NOT CACHED | NOT CACHED | PARQUET | true | hdfs://myhost.com:8020/user/hive/warehouse/airline_data.db/airlines/year=1989 |
| 1990 | 5270893 | 1 | 46.25MB | NOT CACHED | NOT CACHED | PARQUET | true | hdfs://myhost.com:8020/user/hive/warehouse/airline_data.db/airlines/year=1990 |
| 1991 | 5076925 | 1 | 46.77MB | NOT CACHED | NOT CACHED | PARQUET | true | hdfs://myhost.com:8020/user/hive/warehouse/airline_data.db/airlines/year=1991 |
| 1992 | 5092157 | 1 | 48.21MB | NOT CACHED | NOT CACHED | PARQUET | true | hdfs://myhost.com:8020/user/hive/warehouse/airline_data.db/airlines/year=1992 |
| 1993 | 5070501 | 1 | 47.46MB | NOT CACHED | NOT CACHED | PARQUET | true | hdfs://myhost.com:8020/user/hive/warehouse/airline_data.db/airlines/year=1993 |
| 1994 | 5180048 | 1 | 47.47MB | NOT CACHED | NOT CACHED | PARQUET | true | hdfs://myhost.com:8020/user/hive/warehouse/airline_data.db/airlines/year=1994 |
| 1995 | 5327435 | 1 | 62.40MB | NOT CACHED | NOT CACHED | PARQUET | true | hdfs://myhost.com:8020/user/hive/warehouse/airline_data.db/airlines/year=1995 |
| 1996 | 5351983 | 1 | 62.93MB | NOT CACHED | NOT CACHED | PARQUET | true | hdfs://myhost.com:8020/user/hive/warehouse/airline_data.db/airlines/year=1996 |
| 1997 | 5411843 | 1 | 65.05MB | NOT CACHED | NOT CACHED | PARQUET | true | hdfs://myhost.com:8020/user/hive/warehouse/airline_data.db/airlines/year=1997 |
| 1998 | 5384721 | 1 | 62.21MB | NOT CACHED | NOT CACHED | PARQUET | true | hdfs://myhost.com:8020/user/hive/warehouse/airline_data.db/airlines/year=1998 |
| 1999 | 5527884 | 1 | 65.10MB | NOT CACHED | NOT CACHED | PARQUET | true | hdfs://myhost.com:8020/user/hive/warehouse/airline_data.db/airlines/year=1999 |
| 2000 | 5683047 | 1 | 67.68MB | NOT CACHED | NOT CACHED | PARQUET | true | hdfs://myhost.com:8020/user/hive/warehouse/airline_data.db/airlines/year=2000 |
| 2001 | 5967780 | 1 | 74.03MB | NOT CACHED | NOT CACHED | PARQUET | true | hdfs://myhost.com:8020/user/hive/warehouse/airline_data.db/airlines/year=2001 |
| 2002 | 5271359 | 1 | 74.00MB | NOT CACHED | NOT CACHED | PARQUET | true | hdfs://myhost.com:8020/user/hive/warehouse/airline_data.db/airlines/year=2002 |
| 2003 | 6488540 | 1 | 99.35MB | NOT CACHED | NOT CACHED | PARQUET | true | hdfs://myhost.com:8020/user/hive/warehouse/airline_data.db/airlines/year=2003 |
| 2004 | 7129270 | 1 | 123.29MB | NOT CACHED | NOT CACHED | PARQUET | true | hdfs://myhost.com:8020/user/hive/warehouse/airline_data.db/airlines/year=2004 |
| 2005 | 7140596 | 1 | 120.72MB | NOT CACHED | NOT CACHED | PARQUET | true | hdfs://myhost.com:8020/user/hive/warehouse/airline_data.db/airlines/year=2005 |
| 2006 | 7141922 | 1 | 121.88MB | NOT CACHED | NOT CACHED | PARQUET | true | hdfs://myhost.com:8020/user/hive/warehouse/airline_data.db/airlines/year=2006 |
| 2007 | 7453215 | 1 | 130.87MB | NOT CACHED | NOT CACHED | PARQUET | true | hdfs://myhost.com:8020/user/hive/warehouse/airline_data.db/airlines/year=2007 |
| 2008 | 7009728 | 1 | 123.14MB | NOT CACHED | NOT CACHED | PARQUET | true | hdfs://myhost.com:8020/user/hive/warehouse/airline_data.db/airlines/year=2008 |
| Total | 123534969 | 22 | 1.55GB | 0B | | | | |
+-------+-----------+--------+----------+--------------+-------------------+---------+-------------------+----------------------------------------------------------------------------------------------------------+
这时,我们理智地检查一下我们做的分区。所有的分区正好有一个文件,这是偏低的。一个包含WHERE year=2004的子句的查询只会读取一个数据块;该数据块将由一个数据节点读取和处理;因此,对于一个针对单一年份的查询,集群中的所有其他节点都将闲置,而所有工作都发生在一台机器上。甚至有可能在偶然的情况下(取决于HDFS复制因子和数据块在集群中的分布方式),由过滤器选择的多个年份分区,如WHERE year BETWEEN 1999 and 2001,都可能被同一个数据节点读取和处理。每个分区的数据文件越多,就可以获得更多的并行性,在特定节点上发生 "热点 "的概率也就越小,因此,拥有一个大的集群就可以获得更大的性能提升。
但是,数据文件越多,每个数据文件中的数据就越少。如果每个节点只读取几兆的数据,那么在并行查询中分工的开销可能就不值得了。对于Parquet数据块来说,50或100兆是一个不错的大小,9或37兆则偏小。也就是说,我们基于这种分区方案最终得到的数据分布是在合理(合理的大文件)和次优(每个分区的文件很少)之间的边界线上。看看实际效果如何的方法是对着原来的平表和新的分区表运行同样的查询,并比较时间。
剧透:在这种情况下,以我的特定的4节点集群及其特定的数据块分布和我的特定探索性查询,针对分区表的查询确实一直比针对未分区表的相同查询运行得快。但是,如果没有一些实际的测量,我不能确定情况会是这样。下面是我为了得出这个结论而进行的一些查询,首先是针对 airlines_external(没有分区),然后是针对 AIRLINES(按年份分区)。AIRLINES查询的速度一直比较快。改变数据量、改变群集的大小、运行参考或不参考分区键列的查询,或其他因素都可能改变结果,使其有利于一种表布局或另一种表布局。
注意:如果你发现每个分区的体积只有几十兆,可以考虑降低分区的粒度。例如,与其按年、月、日进行分区,不如按年、月甚至只按年进行分区。在并行查询中,高效分配工作的理想布局是每个Parquet文件几十兆甚至上百兆,而且每个分区的Parquet文件数要比数据节点数高一些。
> SELECT SUM(airtime) FROM airlines_external;
+--------------+
| 8662859484 |
+--------------+
> SELECT SUM(airtime) FROM airlines;
+--------------+
| 8662859484 |
+--------------+
> SELECT SUM(airtime) FROM airlines_external WHERE year = 2005;
+--------------+
| 708204026 |
+--------------+
> SELECT SUM(airtime) FROM airlines WHERE year = 2005;
+--------------+
| 708204026 |
+--------------+
现在我们终于可以分析这个数据集了,从原始数据文件中,我们不知道它们包含哪些列。让我们来看看一个航班的飞行时间是否会根据一周中的某一天而趋于不同。我们可以看到,第6天的平均数要高一些,可能周六是飞行繁忙的一天,飞机在降落前要在目的地机场盘旋较长时间。
> SELECT dayofweek, AVG(airtime) FROM airlines
GROUP BY dayofweek ORDER BY dayofweek;
+-----------+-------------------+
| dayofweek | avg(airtime) |
+-----------+-------------------+
| 1 | 102.1560425016671 |
| 2 | 102.1582931538807 |
| 3 | 102.2170009256653 |
| 4 | 102.37477661846 |
| 5 | 102.2697358763511 |
| 6 | 105.3627448363705 |
| 7 | 103.4144351202054 |
+-----------+-------------------+
为了看看这种明显的趋势是否会随着时间的推移而保持下去,让我们按一周中的某一天来做同样的细分,但也按年份来划分。现在我们可以看到,第6天的平均播放时间在每一年都会持续增加。我们也可以看到,随着时间的推移,平均播放时间全面增加。而1987年到1994年这一列出现了NULL,说明涉及这一列的查询需要限制在1995年及以上的日期范围。
> SELECT year, dayofweek, AVG(airtime) FROM airlines
GROUP BY year, dayofweek ORDER BY year DESC, dayofweek;
+------+-----------+-------------------+
| year | dayofweek | avg(airtime) |
+------+-----------+-------------------+
| 2008 | 1 | 103.1821651651355 |
| 2008 | 2 | 103.2149301386094 |
| 2008 | 3 | 103.0585076622796 |
| 2008 | 4 | 103.4671383539038 |
| 2008 | 5 | 103.5575385182659 |
| 2008 | 6 | 107.4006306562128 |
| 2008 | 7 | 104.8648851041755 |
| 2007 | 1 | 102.2196114337825 |
| 2007 | 2 | 101.9317791906348 |
| 2007 | 3 | 102.0964767689043 |
| 2007 | 4 | 102.6215927201686 |
| 2007 | 5 | 102.4289399000661 |
| 2007 | 6 | 105.1477448215756 |
| 2007 | 7 | 103.6305945644095 |
...
| 1996 | 1 | 99.33860750862108 |
| 1996 | 2 | 99.54225446396656 |
| 1996 | 3 | 99.41129336113134 |
| 1996 | 4 | 99.5110373340348 |
| 1996 | 5 | 99.22120745027595 |
| 1996 | 6 | 101.1717447111921 |
| 1996 | 7 | 99.95410136133704 |
| 1995 | 1 | 96.93779698300494 |
| 1995 | 2 | 96.93458674589712 |
| 1995 | 3 | 97.00972311337051 |
| 1995 | 4 | 96.90843832024412 |
| 1995 | 5 | 96.78382115425562 |
| 1995 | 6 | 98.70872826057003 |
| 1995 | 7 | 97.85570478374616 |
| 1994 | 1 | NULL |
| 1994 | 2 | NULL |
| 1994 | 3 | NULL |
...
| 1987 | 5 | NULL |
| 1987 | 6 | NULL |
| 1987 | 7 | NULL |
+------+-----------+-------------------+