# 六、ETL 和 Hive

当您拥有想要加载到 Hive 中的现有数据，而不需要使用外部表创建到源数据的链接时，Hive 提供了许多选项。事实上，Hive 有几个命令来支持 ETL，所有这些都导致填充一个内部 Hive 表。

您可以从 HBase 的数据子集填充 Hive，并将行键拆分为用于索引的部分，也可以从 HDFS 或本地文件系统加载文件。作为负载的一部分，您可以将数据转换为更有用的表示，或者只去除您需要的部分。

Hive 为 ETL 提供了多个选项，但是所有的处理都是通过 map/reduce 作业完成的，这意味着加载任何大小的数据都是可能的。Hive 特别适合替代数据输入方法 ETL，因为它支持以本机格式非常高效地加载数据，然后可以通过读写 Hive 表来转换数据。

在本章中，我们将介绍以您选择的格式将数据输入 Hive 的主要命令。

最简单的 ETL 工具是加载命令。简单地说，它将一个或一组文件加载到现有的内部 Hive 表中。只有当源文件中的数据与目标表模式匹配时，才适合运行 load，因为您不能在此选项中包含任何转换。

您还应该知道，load 语句不会进行任何验证，这意味着您可以将格式错误的文件加载到表中，命令将会运行，但您将无法再次读回数据。

加载时，您可以使用本地文件路径或 HDFS 路径指定源文件。代码清单 47 显示了一个示例，该示例将一个 syslog 文件从本地/tmp 目录加载到 syslogs_flat 表中，然后获取第一行。

 47:将数据加载到配置单元

```
  > load data local inpath '/tmp/sample-data/syslogs/syslog'
  into table syslogs_flat;
  …
  INFO  : Loading data to table
  default.syslogs_flat from file:/tmp/syslogs/syslog
  INFO  : Table default.syslogs_flat stats:
  [numFiles=1, totalSize=462587]
  No rows affected (0.153 seconds)

  > select * from syslogs_flat limit 1;
  +------------------------------------------------------------------------+-
  |                          
  syslogs_flat.entry                           |
  +------------------------------------------------------------------------+-
  | Jan 28 20:35:00 sc-ub-xps thermald[785]:
  Dropped below poll threshold  |
  +------------------------------------------------------------------------+--+

```

load 命令有两个用于更改其行为的限定符:

*   本地—指定源文件路径在本地计算机上。如果省略，文件路径假定为 HDFS。
*   覆盖—在加载新文件之前，删除表格中的现有内容。如果省略，新数据将追加到表中。

代码清单 48 显示了 load 命令的另一种用法——将 syslogs_flat 表中的现有数据附加到 HDFS 的一个文件中，该文件在加载前后都有计数，以便显示表中的行数。

 48:将数据追加到配置单元

```
  > select count(*) from syslogs_flat;
  +-------+--+
  |  _c0  |
  +-------+--+
  | 3942  |
  +-------+--+
  1 row selected (16.474 seconds)
  > load data inpath 'hdfs://localhost:9000/tmp/syslog.1' into
  table syslogs_flat;
  INFO  : Loading data to table
  default.syslogs_flat from hdfs://localhost:9000/tmp/syslog.1
  INFO  : Table default.syslogs_flat stats:
  [numFiles=2, totalSize=1753418]
  No rows affected (0.195 seconds)
  > select count(*) from syslogs_flat;
  +--------+--+
  |  _c0   |
  +--------+--+
  | 15642  |
  +--------+--+

```

| ![](img/00005.gif) | 注意:源文件路径在这里被指定为完整的 URI，以便显示可以使用远程 HDFS 集群，但是您也可以为 Hive 使用的主 HDFS 集群中的文件指定相对或绝对路径。 |

load 命令只能用于内部表，因为它的单一功能是将文件从指定的源复制到 HDFS 的 Hive 表的基础文件夹中。代码清单 49 显示了前面两个加载操作之后表格文件夹的内容。

 49:列出加载到配置单元的文件

```
  root@hive:/hive-setup# hdfs dfs -ls
  /user/hive/warehouse/syslogs_flat
  Found 2 items
  -rwxrwxr-x   1 root supergroup     462587 2016-02-02 07:28
  /user/hive/warehouse/syslogs_flat/syslog
  -rwxrwxr-x   1 root supergroup    1290831 2016-02-02 07:32
  /user/hive/warehouse/syslogs_flat/syslog.1

```

使用覆盖标志，Hive 在复制新的源文件之前删除现有文件。代码清单 50 显示了用新文件覆盖表的结果，还显示了加载后的 HDFS 文件清单。

 50:覆盖加载

```
  > load data inpath 'hdfs://localhost:9000/tmp/syslog.2.gz'
  overwrite into table syslogs_flat;
  INFO  : Loading data to table
  default.syslogs_flat from hdfs://localhost:9000/tmp/syslog.2.gz
  INFO  : Table default.syslogs_flat stats:
  [numFiles=1, numRows=0, totalSize=253984, rawDataSize=0]
  No rows affected (0.154 seconds)
  > select count(*) from syslogs_flat;
  +--------+--+
  |  _c0   |
  +--------+--+
  | 15695  |
  …
  root@hive:/hive-setup# hdfs dfs -ls
  /user/hive/warehouse/syslogs_flat         
  Found 1 items
  -rwxrwxr-x   1 root supergroup     253984
  2016-02-02 07:44 /user/hive/warehouse/syslogs_flat/syslog.2.gz 

```

load 语句是一种将数据加载到 Hive 中的强大而快速的方法，因为它只是执行一个 HDFS put，将文件从源复制到 Hive 表结构中。但是，它的使用仅限于源文件格式正确且加载过程中不能发生转换的情况。

| ![](img/00005.gif) | 注意:Hive 还具有导入和导出功能，允许您将表保存或加载到 HDFS 位置。这些不同于加载，因为 Hive 元数据与数据一起导出和导入，所以您可以有效地使用这些函数在一个 Hive 实例上备份表，并在另一个 Hive 实例上重新创建表。 |

我们可以看到在 syslogs_flat 表中有未转换的数据的局限性。例如，来自 Linux 机器的系统日志文件可以很容易地加载，但是格式不容易在 Hive 中映射。每个条目使用空格分隔的日期、机器名称和事件类型字段，以及消息前的冒号。数据不能作为加载的一部分进行转换，所以我有一个带有单个字符串列的 Hive 表，其中每行都包含一个字符串值中的所有日志条目数据。

如果我们使用 load，syslogs_flat 表中的行不太适合查询，但是现在数据在 Hive 中，我们可以使用其他选项将数据转换为更有用的格式，并将其加载到其他表中。

这更像是一种提取、加载、转换(ELT)的方法，首先将数据以其本地格式加载到 Hive 中(速度很快)，然后进行转换。转换可以通过 Hive 作为映射/缩减作业进行调度。

Hive 支持用对其他对象的查询结果加载一个表，并且您可以在查询中包含转换函数。您也可以通过这种方式填充内部或外部表，查询解析器进行一些基本验证，以确保源查询中的列适合目标表。

列在源和目标之间位置匹配，这意味着您需要精心设计 select 语句，以便结果集中的列与目标表中定义的列顺序相同。

如果查询中的列太多或太少，Hive 将不会尝试将它们与目标表匹配，您将会得到一个错误，如代码清单 51 所示。

 51:无效的插入语句

```
  > insert into table server_logs select 's1', 123L, 'E' as
  loglevel from dual;
  Error: Error while compiling statement: FAILED: SemanticException
  [Error 10044]: Line 1:18 Cannot insert into target table because column
  number/types are different 'server_logs': Table insclause-0 has 4 columns,
  but query has 3 columns. (state=42000,code=10044)

```

| ![](img/00009.jpeg) | 提示:必须在 insert…select 语句中包含 from 子句，这意味着不能使用诸如 select 1、' a '之类的文字表达式。但是您可以创建一个具有一行一列的表(我用 Oracle 数据库中的约定将其命名为 dual)，然后可以在 from 子句中使用 dual。你会发现在[表格]中插入 会失败，但是在[表格]中插入从 dual 中选择‘a’、‘b’、‘3’会成功。 |

Hive 不验证列的数据类型，这意味着您可以将数据加载到错误的列中，而不会出现任何错误。当您尝试查询数据并且 Hive 无法映射值时，类型不匹配将会表现出来，因此结果包含空值。

查询子句可以包含任何有效的 HiveQL，包括内部和外部表之间的连接和联合、函数调用和聚合，这允许大量的提取和转换逻辑。为了使一组系统日志文件更有用，我为数据项定义了一个带有单独列的新表，如代码清单 52 所示。

 52:系统日志的结构化表

```
  > describe syslogs;
  +-----------+------------+----------+--+
  | col_name  | data_type  | comment  |
  +-----------+------------+----------+--+
  | loggedat  | timestamp  |          |
  | host      | string     |          |
  | process   | string     |          |
  | pid       | int        |          |
  | message   | string     |          |
  +-----------+------------+----------+--+

```

我们将在第 9 章使用 HiveQL 查询中看到更多的 HiveQL 函数，有一个有用的字符串函数叫做句子，它接受一个输入字符串，并将其标记为一个句子数组，每个句子包含一个单词数组。我可以用它来从日志条目中提取特定的单词，如代码清单 53 所示，其中我还将字符串转换为其他类型。

 53:拆分字符串字段

```
  > select sentences(entry)[0][5] as host,
  sentences(entry)[0][6] as process, cast(sentences(entry)[0][7] as int) as pid
  from syslogs_flat limit 5;
  +------------+------------------------------------+-------+--+
  |    host    |             
  process               |  pid  |
  +------------+------------------------------------+-------+--+
  | sc-ub-xps  |
  anacron                            | 804   |
  | sc-ub-xps  |
  anacron                            | 804   |
  | sc-ub-xps  |
  org.gnome.zeitgeist.SimpleIndexer  | 1395  |
  | sc-ub-xps  |
  systemd-timesyncd                  | 625   |
  | sc-ub-xps  |
  systemd                            | 1293  |
  +------------+------------------------------------+-------+--+

```

日志条目的时间戳有点复杂，尤其是因为 Ubuntu 没有在日志中记录年份，但是如果我们假设当前年份，我们可以使用字符串和日期函数的组合将年份添加到条目中的日期和时间之前，然后将其全部转换为时间戳，如代码清单 53 所示。

 54:将字符串转换为时间戳

```
  > select unix_timestamp(concat(cast(year(current_date) as
  string), ' ', substr(entry, 0, 15)), 'yyyy MMM dd hh:mm:ss') from
  syslogs_flat limit 1; 
  +-------------+--+
  |     _c0     |
  +-------------+--+
  | 1453755712  |
  +-------------+--+

```

最后一列是日志消息，它是一个简单的子字符串，位于进程标识的右方括号之后，如代码清单 55 所示。

 55:提取子字符串

```
  > select trim(substr(entry, instr(entry, ']')+2)) from
  syslogs_flat limit 1; 
  +------------------------------+--+
  |             _c0              |
  +------------------------------+--+
  | Job 'cron.daily' terminated  |
  +------------------------------+--+

```

将所有这些放在一起，我们可以用一个 insert … select 从原始 syslogs_flat 表填充新的 syslogs 表。由于输入数据的性质，查询方面很笨拙，但是一旦它运行，我们就可以在格式化的 syslogs 表上做出更有用的选择，如代码清单 56 所示。

 56:转换原始数据

```
  > insert into syslogs select
  unix_timestamp(concat(cast(year(current_date) as string), ' ', substr(entry,
  0, 15)), 'yyyy MMM dd hh:mm:ss'), sentences(entry)[0][5],
  sentences(entry)[0][6], cast(sentences(entry)[0][7] as int),
  trim(substr(entry, instr(entry, ']')+2)) from syslogs_flat;
  ...
  No rows affected (16.757 seconds)
  > select process, count(process) as entries from syslogs
  where host = 'sc-ub-xps'  group by process order by entries desc limit 5;
  …
  +-----------------+----------+--+
  |     process     | entries  |
  +-----------------+----------+--+
  | kernel          | 7862     |
  | thermald        | 2906     |
  | systemd         | 1450     |
  | NetworkManager  | 1245     |
  | avahi-daemon    | 310      |
  +-----------------+----------+--+

```

向表中插入查询结果还支持 overwrite 子句，该子句可以在插入查询结果之前有效地截断目标表。

Hive 支持 insert … select 的扩展版本，在该版本中，可以链接来自同一源上的多个查询的多个 insert 语句。在这个变体中，您将从指定源表(或视图)开始，然后添加插入。

当您需要从单个源填充不同的 Hive 投影时，多个插入非常有用，因为多个插入可以非常高效地运行。Hive 将扫描一次源数据，然后对扫描的数据运行每个查询，将其插入到目标中。

在代码清单 57 中，我们使用未格式化的 syslogs 表作为源，并同时加载格式化的表和一个汇总表。

 57:多个插入

```
  from syslogs_flat sf
  insert overwrite table syslogs select
  unix_timestamp(concat(cast(year(current_date) as string), ' ',
  substr(sf.entry, 0, 15)), 'yyyy MMM dd hh:mm:ss'), sentences(sf.entry)[0][5],
  sentences(sf.entry)[0][6], cast(sentences(sf.entry)[0][7] as int),
  trim(substr(sf.entry, instr(sf.entry, ']')+2))
  insert overwrite table syslog_summaries select
  unix_timestamp(), sentences(sf.entry)[0][5] as host,
  count(sentences(sf.entry)[0][5]) as entries group by
  sentences(sf.entry)[0][5]
  …
  > select * from syslog_summaries limit 1;
  +-------------------------------+------------------------+-----------------
  | syslog_summaries.processedat  |
  syslog_summaries.host  | syslog_summaries.entries  |
  +-------------------------------+------------------------+-----------------
  | 2016-02-02 20:00:48.062       |
  sc-ub-xps              | 15695                     |
  +-------------------------------+------------------------+-----------------

```

从查询结果中插入数据的最后一个变体是创建表为 select (CTAS)，它允许我们定义一个表并用一条语句填充它。表只能是内部的，但是我们可以为内部表指定 normal stored by 子句。

在 CTAS 表中，结构是从查询中推断出来的，这意味着我们不需要显式指定列。查询应该将值转换为我们想要的目标表的数据类型，并且应该指定将用作列名的别名。

CTAS 操作是原子的，这意味着在 CTAS 操作完成并填充表之前，表不会显示为可供查询。

代码清单 58 显示了一个 CTAS 语句，用于将 syslog 条目中的单个单词加载到一个新表中。select 语句将日志消息解析为句子，并将第一个句子提取为字符串数组，这就是 Hive 定义列的方式。

 58:将表格创建为选择

```
  > create table syslog_sentences stored as orc as select
  sentences(trim(substr(entry, instr(entry, ']')+2)))[0] words from
  syslogs_flat;
  ...
  No rows affected (12.758 seconds)
  > describe syslog_sentences;
  +-----------+----------------+----------+--+
  | col_name  |   data_type    | comment  |
  +-----------+----------------+----------+--+
  | words     | array<string>  |          |
  +-----------+----------------+----------+--+
  1 row selected (0.064 seconds)
  > select * from syslog_sentences limit 2;
  +------------------------------------+--+
  |       syslog_sentences.words       |
  +------------------------------------+--+
  |
  ["Job","cron.daily","terminated"]  |
  |
  ["Normal","exit","1","job","run"] 
  |
  +------------------------------------+--+

```

Hive 支持临时表，这对于 ETL/ELT 工作负载中的临时数据转换非常有用，这些工作负载无法在一个步骤中实现完全转换。临时表不支持索引，它们只在 Hive 会话期间存在——会话结束时会自动删除。

临时表是为用户存储在 HDFS 工作目录中的内部表。可以使用受支持的文件格式来指定它们，以便您在使用该表时受益于高效的存储。但是，请注意，并非所有功能都受支持。

代码清单 59 显示了一个临时表，用来存储 ETL 作业的进度。

 59:使用临时表

```
  > create temporary table etl_progress(status string, stage
  string, processedat timestamp, rowcount bigint) stored as orc;
  No rows affected (0.079 seconds)
  > insert into etl_progress(status, stage, processedat,
  rowcount) values('Done', 'Transform.1', '2016-02-02 07:03:01', 328648);
  No rows affected (14.853 seconds)
  > select * from etl_progress;
  +----------------------+---------------------+---------------------------+-
  | etl_progress.status  | etl_progress.stage  |
  etl_progress.processedat  | etl_progress.rowcount  |
  +----------------------+---------------------+---------------------------+-
  | Done                 | Transform.1         |
  2016-02-02 07:03:01.0     | 328648                 |
  +----------------------+---------------------+---------------------------+-

```

在这种情况下，insert 语句使用文字日期，因为 Hive 不支持在 values 子句中使用函数来插入临时表。如果您尝试使用内置函数(如 unix_timestamp)来获取当前时间，您将会收到一个错误。

然而，您可以在 select 子句中使用函数，这意味着您可以使用同样的技巧从 dual 中选择文字，如代码清单 60 所示。

 60:从函数插入

```
  > insert into etl_progress(status, stage, processedat,
  rowcount) values('Done', 'Transform.1', unix_timestamp(), 328648);
  Error: Error while compiling statement: FAILED: SemanticException
  [Error 10293]: Unable to create temp file for insert values Expression of
  type TOK_FUNCTION not supported in insert/values (state=42000,code=10293)
  > insert into etl_progress select 'Done', 'Transform.2',
  unix_timestamp(), 12435358 from dual;
  ...
  No rows affected (12.437 seconds)

```

无论会话是与 Beeline 交互还是通过外部接口提交作业，Hive 都会在会话结束时删除该表。临时表仅在创建它的会话中可见。其他会话，即使是同一用户，也不会看到该表。

当创建临时表的会话结束时，表的数据和元数据被删除，当下一个会话开始时，表将消失，如代码清单 61 所示。

 61:临时表被删除

```
  > select * from etl_progress;
  +----------------------+---------------------+---------------------------+-
  | etl_progress.status  | etl_progress.stage  |
  etl_progress.processedat  | etl_progress.rowcount  |
  +----------------------+---------------------+---------------------------+-
  | Done                 | Transform.1         |
  2016-02-02 07:03:01.0     | 328648                 |
  | Done                 | Transform.2         |
  1970-01-17 20:01:23.674   | 12435358               |
  +----------------------+---------------------+---------------------------+-2
  rows selected (0.091 seconds)
  > !close
  Closing: 0: jdbc:hive2://127.0.0.1:10000
  beeline> !connect jdbc:hive2://127.0.0.1:10000 -n root
  Connecting to jdbc:hive2://127.0.0.1:10000
  …
  > select * from etl_progress;
  Error: Error while compiling statement: FAILED: SemanticException
  [Error 10001]: Line 1:14 Table not found 'etl_progress'
  (state=42S02,code=10001)

```

在本例中，当直线用户断开与！命令，会话结束，Hive 服务器删除临时表。当用户再次连接时，新的会话开始，临时表将消失。

通过基本的加载、插入和 CTAS 语句，Hive 支持将数据放入仓库的主要模式。如果您有提取和转换数据的现有流程，您可以将这些流程直接加载到 Hive 表中。这种快速操作将导致数据被安全地存储在 HDFS，并可通过 Hive 进行查询。

对于新的数据加载，ELT 过程通过最初使用 load 将原始数据放入 Hive 表并使用 HiveQL 对其进行转换，从而更好地利用 Hive。insert … select 和 create table … as select 语句允许您创建一个复杂的查询，该查询带有转换数据的函数，并通过可扩展的映射/缩减作业将其填充到 Hive 中。

一旦您在 Hive 中将数据作为表，您就可以创建视图和索引来使信息变得可访问。提取下一组数据时，只需重复插入，新数据将被追加到现有的表中。

在下一章中，我们将更仔细地研究使用 HiveQL(Hive Query Language)定义对象和修改数据。