03 离线利器:大数据离线处理工具 Hive 的常用技巧

今天为你介绍数据分析师最常用的数据处理工具 Hive 的一些使用技巧。这些技巧我们在工作中使用得比较频繁,如果运用得当,将为我们省去不少时间精力。

那么首先,我们先来了解下 Hive。Hive 是 Facebook 开源的一款基于 Hadoop 的数据仓库工具,它能完美支持 SQL 查询功能,将 SQL 查询转变为 MapReduce 任务执行。这使得大数据统计得以实现。Hive 是最早的也是目前应用最广泛的大数据处理解决方案。

Hive 的重要性不必多言。 数据分析师在工作中使用 Hive SQL 来处理大数据本是家常便饭。

本课时将重点介绍 Hive 在工作中常用的数据处理技巧。

基本操作

这里简单介绍 Hive 最基础的操作,创建表加载数据。重点说明 Hive 创建内部表和外部表的应用场景。

创建表

创建表的语法如下:

CREATE [EXTERNAL] TABLE [IF NOT EXISTS] table_name
  [(col_name data_type [COMMENT col_comment], ...)]
  [COMMENT table_comment]
  [PARTITIONED BY (col_name data_type [COMMENT col_comment], ...)]
  [CLUSTERED BY (col_name, col_name, ...) [SORTED BY (col_name [ASC|DESC], ...)] INTO num_buckets BUCKETS]
  [
   [ROW FORMAT row_format] [STORED AS file_format]
   | STORED BY 'storage.handler.class.name' [ WITH SERDEPROPERTIES (...) ]
  ]
  [LOCATION hdfs_path]

CREATE [EXTERNAL] TABLE [IF NOT EXISTS] table_name
  [(col_name data_type [COMMENT col_comment], ...)]
  [COMMENT table_comment]
  [PARTITIONED BY (col_name data_type [COMMENT col_comment], ...)]
  [CLUSTERED BY (col_name, col_name, ...) [SORTED BY (col_name [ASC|DESC], ...)] INTO num_buckets BUCKETS]
  [
   [ROW FORMAT row_format] [STORED AS file_format]
   | STORED BY 'storage.handler.class.name' [ WITH SERDEPROPERTIES (...) ]
  ]
  [LOCATION hdfs_path]

上面的代码看起来不是很容易理解,这里通过两个例子重点讲下如何按照上面语法进行 Hive 表创建,并且重点说下创建内部表外部表的区别。

  • 内部表

第一步,创建一个内部表,按天分区,字段直接以'\t'分割:

create table table_test_1  -- 这里没有标注external创建的表, 就是内部表
	uid string comment '用户id',
	age string comment '用户年龄',
	gender string comment '用户性别'
PARTITIONED BY(dt STRING)
ROW FORMAT DELIMITED
  FIELDS TERMINATED BY '\t'
STORED AS Textfile

create table table_test_1  -- 这里没有标注external创建的表, 就是内部表
	uid string comment '用户id',
	age string comment '用户年龄',
	gender string comment '用户性别'
PARTITIONED BY(dt STRING)
ROW FORMAT DELIMITED
  FIELDS TERMINATED BY '\t'
STORED AS Textfile

第二步,向刚创建的表加载数据,有本机数据加载和 HDFS 路径数据加载两种方式。本机本地数据导入刚创建的表方式如下:

LOAD DATA LOCAL INPATH '本机文件路径' OVERWRITE INTO TABLE table_test_1 PARTITION(dt='20200822');

LOAD DATA LOCAL INPATH '本机文件路径' OVERWRITE INTO TABLE table_test_1 PARTITION(dt='20200822');

HDFS 路径加载数据方式如下。

LOAD DATA INPATH 'hdfs文件路径' OVERWRITE INTO TABLE table_test_1 PARTITION(dt='20200822');

LOAD DATA INPATH 'hdfs文件路径' OVERWRITE INTO TABLE table_test_1 PARTITION(dt='20200822');
  • 外部表

外部表创建,即为某路径下的文件指定为一个 Hive 表结构,这种外部表的创建,不涉及数据的移动。

外部表创建有两种方式,第一种,就是在建表的时候就指定外部表的数据依赖路径。下面的代码即做了这方面的展示:

-- 第一步, 创建外部表:
create external table table_test_2  -- 这里标注external创建的表, 就是外部表
	uid string comment '用户id',
	age string comment '用户年龄',
	gender string comment '用户性别'
ROW FORMAT DELIMITED
  FIELDS TERMINATED BY '\t'
LOCATION 'HDFS路径'   -- 这里指定集群中某个路径的文件为该外部表数据源

-- 第一步, 创建外部表:
create external table table_test_2  -- 这里标注external创建的表, 就是外部表
	uid string comment '用户id',
	age string comment '用户年龄',
	gender string comment '用户性别'
ROW FORMAT DELIMITED
  FIELDS TERMINATED BY '\t'
LOCATION 'HDFS路径'   -- 这里指定集群中某个路径的文件为该外部表数据源

第二种,是先创建一个外部表结构,然后再指定路径。

-- 第一步, 创建外部表:
create external table table_test_2  -- 这里标注external创建的表, 就是外部表
	uid string comment '用户id',
	age string comment '用户年龄',
	gender string comment '用户性别'
ROW FORMAT DELIMITED
  FIELDS TERMINATED BY '\t'

-- 第一步, 创建外部表:
create external table table_test_2  -- 这里标注external创建的表, 就是外部表
	uid string comment '用户id',
	age string comment '用户年龄',
	gender string comment '用户性别'
ROW FORMAT DELIMITED
  FIELDS TERMINATED BY '\t'

上面我们通过代码展示了 Hive 表的创建,下面来讲解下 Hive 复合类型数据。

复合类型数据

对于 Hive 的基础数据结果,比如 string、bigint 等比较简单,这里直接跳过。下面经哥讲下 Hive 的复合类型数据用法。即 map、array、json,这三个复合数据类型要相对有一点点难度,可在工作中我们会经常使用。

map

首先是 map。map 的数据结构是 key-value 格式。数据语法为 map(k1,v1,k2,v2,…)。使用给定的 key-value 对,构造一个 map 数据结构。

下面这个例子表现的就是 map 构建。

hive> select map('k1','v1','k2','v2'), map('k1,'v1','k2','v2')['k1'] -- 获取k1对应值, map'k1',v1','k2','v2')['k2'] -- 获取k2对应值from tble_tet;OK{"k2""v","k1:"1"} v1 v2

hive> select map('k1','v1','k2','v2'), map('k1,'v1','k2','v2')['k1'] -- 获取k1对应值, map'k1',v1','k2','v2')['k2'] -- 获取k2对应值from tble_tet;OK{"k2""v","k1:"1"} v1 v2

工作中,map 样式数据被建表的同学声明为 string。这时候如果要使用 map 类型的特性,需要先将 string 转化为 map 类型。那么首先,我们要将下面字符串 'zhang:101&wang:102&li:103' 转化为 map,就需要使用 str_to_map 这个函数:

- str_to_map(your_string, delimiter1, delimiter2)
-- your_string: 是准备要转化的字符串
-- delimiter1: 将字符串切分成KV格式
-- 每个KV的分隔符

- str_to_map(your_string, delimiter1, delimiter2)
-- your_string: 是准备要转化的字符串
-- delimiter1: 将字符串切分成KV格式
-- 每个KV的分隔符

举个例子,我们可以看一下这个代码:

select str_to_map('zhang:101&wang:102&li:103', '&', ':');  -- 这里注意第2、3个参数
>>> 输出如下: 
{"zhang":"101","wang":"102","li":"103"}

select str_to_map('zhang:101&wang:102&li:103', '&', ':');  -- 这里注意第2、3个参数
>>> 输出如下: 
{"zhang":"101","wang":"102","li":"103"}

以上为 map 构建过程。
array

了解过 map 后,我们再来了解一下 array。array 的语法为 array(val1,val2,val3,…),操作类型为 array。我们可以使用给定的表达式,构造一个 array 数据结构。举例:

hive> select array(100,200,300)
, array(100,200,300)[0]
, array(100,200,300)[1]
from table_test;
OK
[100,200,300] 100 200

hive> select array(100,200,300)
, array(100,200,300)[0]
, array(100,200,300)[1]
from table_test;
OK
[100,200,300] 100 200

刚才我们说到实际工作的表,几乎都是默认使用 string 存储,很少在创建表的时候使用 array,往往都是基于 string 生成 array 进行使用的。那么,什么时候会用到 array 类型呢?下面我们举两个使用 array 类型的实例:

  • split 切分字符串返回 array 类型。

第一个例子为 split 切分字符串返回 array 类型。如下代码所示。

hive> select split('zhang:wang:li', ':')    -- 即:将字符串切分为array
, split('zhang:wang:li', ':')[0]            -- 即:将字符串切分为array, 并获取array第一个元素
, split('zhang:wang:li', ':')[1]            -- 即:将字符串切分为array, 并获取array第二个元素
from table_test;
OK
["zhang","wang","li"] zhang wang

hive> select split('zhang:wang:li', ':')    -- 即:将字符串切分为array
, split('zhang:wang:li', ':')[0]            -- 即:将字符串切分为array, 并获取array第一个元素
, split('zhang:wang:li', ':')[1]            -- 即:将字符串切分为array, 并获取array第二个元素
from table_test;
OK
["zhang","wang","li"] zhang wang
  • collect_list 聚合某字段返回 array 类型。

我们假设一张记录用户登录使用手机型号的表,如下所示:

hive> select userId, phone from table_test;
OK
10001 MI_1
10001 MI_2
10001 MI_3
10001 MI_4
10002 MIX_2
10002 IPHONE11
10002 HUAWEI

hive> select userId, phone from table_test;
OK
10001 MI_1
10001 MI_2
10001 MI_3
10001 MI_4
10002 MIX_2
10002 IPHONE11
10002 HUAWEI

随后,我们对不同用户的不同终端类型以及终端数进行统计。

select userId
, sort_array(collect_set(phone)) as phone_array   -- 这里将同一个用户使用的终端类型情况聚合到一个数组内并去除重复的手机型号, 最后再排序
, size(collect_set(phone)) as phone_size   --  将用一个用户使用手机类型去重后求数组大小, 即为用户使用不同终端数量
from table_test
group by userId

需要注意的是collect_set、collect_list。这里生成 array、sort_array 和 size 函数,是为了对生成的 array 进行处理。
在工作中,这样的应用场景很多。我们可以把手机终端替换成 IP 地址、省份、兴趣标签等,得出不同的业务场景。比如,分析一个用户在 1 天或者 1 分钟内切换 IP 地址的数量,用来作为一个公司反作弊的数据指标;分析下单用户身上的兴趣标签,用来为业务方提供优质的决策。

json

再来看下 json,json 同样是我们使用较多的数据。因为 json 具有良好的可读性和便于快速编写的特点,可以在不同平台之间进行数据交换。所以 json 数据类型在开发接口的时候应用最广。

语法使用 get_json_object(string json_string, string path)。返回值使用 string。json 字符串可被 Hive 存储,解析可以直接使用 get_json_object。需要说明的是,解析 json 的字符串 json_string,则返回 path 指定的内容。如果输入的 json 字符串无效,则返回 NULL。

下面两个例子示范 json 字符串是如何解析使用的。

第一个例子相对简单,只解析 json 第一层:

hive> select get_json_object('{"data":
{"teachers":\[{"gender":1,"age":"30"},{"gender":0,"age":"35"}\],
"students":\[{"gender":1,"age":"10"},{"gender":0,"age":"11"}\]
},"city_name":"beijing"', '$.city_name');
>>> 输出:
beijing

hive> select get_json_object('{"data":
{"teachers":\[{"gender":1,"age":"30"},{"gender":0,"age":"35"}\],
"students":\[{"gender":1,"age":"10"},{"gender":0,"age":"11"}\]
},"city_name":"beijing"', '$.city_name');
>>> 输出:
beijing

这个例子目的在于让你了解 get_json_object 的基本用法。

第二个例子相对复杂。这是一个嵌套 json array 的解析过程。需要你花时间学习、理解。以后再遇到 json 嵌套问题,都可以用这种方法解决。

这个例子中的数据如下:

hive> select json_sample from table_test;
OK
-- 返回json字符串,记录信息为用户在某个时间访问了不同城市旅游产品信息,为方便阅读,已经过json格式化处理:
{
"time": 1597920700558,
"from": "104",
"uid": "124789",
"ip": "198.168.0.52",
"data": [{
"post_id": "{\"city_id\":\"10001\",\"time\":1597920700558\"}",
"type": 3
}, {
"post_id": "{\"city_id\":\"10002\",\"time\":1597920700558\"}",
"type": 8
}]
}

hive> select json_sample from table_test;
OK
-- 返回json字符串,记录信息为用户在某个时间访问了不同城市旅游产品信息,为方便阅读,已经过json格式化处理:
{
"time": 1597920700558,
"from": "104",
"uid": "124789",
"ip": "198.168.0.52",
"data": [{
"post_id": "{\"city_id\":\"10001\",\"time\":1597920700558\"}",
"type": 3
}, {
"post_id": "{\"city_id\":\"10002\",\"time\":1597920700558\"}",
"type": 8
}]
}

这里的目的是将用户访问过的城市 id 提取出来,并进行行转列操作。

hive> select uid, split(get_json_object(get_json_object(json_sample, '$.post_id'),'$.city_id'),'_')[0] as city_id
from table_test
lateral view explode(split(
regexp_replace(
regexp_replace(
get_json_object(data, '$.data'),
'\\[|\\]',''),  --将 json 数组两边的中括号去掉。 这里需要特别注意反斜杠\在hive中的用法。

‘\}\,\{’ --将 json 数组元素之间的逗号换成分号, 因为data内部是多个 json 串,不同 json 串也是用逗号分隔,这个容易和下一层分隔符混淆
,‘\};\{’),

‘\;’)) tb as json_sample – 将 json 数组切分成一个个 json 结构的字符串,以使用 get_json_object 进行解析
– 最后输出结果如下:
124789 10001
124789 10002

可以看出,上面 SQL 解析出 json 中 uid、city_id, 并将一行转为两行输出。这个案例分享的是对于嵌套 json 的解析过程。如果可以理解并掌握,那么以后 Hive 中涉及 json 数据解析都不会是问题。

特殊数据处理场景

除去上述场景外,还有一些特殊的数据处理场景。下面我们介绍一个较为典型的时间函数

时间函数

时间类型,应用场景非常广泛,处理也很有技巧性。

针对工作中经常使用的日期转化和运算,我分享一个例子,相信对你很有帮助。

首先,我讲下日期格式转化:

  • 时间戳转化日期:from_unixtime(bigint unixtime, string format)。

这里我们需要两个参数。

参数1:unixtime 为时间戳,即从 19700101 开始至今累计的秒数。

参数2:是希望转行日期的格式,比如年月日可表示为"yyyy-MM-dd"。

-- 注意下面:将字符串可以按照第二个参数定制的样式输出
hive> select from_unixtime(1598360886, 'yyyy-MM-dd HH:mm:ss')
, from_unixtime(1598360886, 'yyyy-MM-dd HH')
, from_unixtime(1598360886, 'yyyy-MM-dd')
, from_unixtime(1598360886, 'yyyyMMdd')
OK

-- 注意下面:将字符串可以按照第二个参数定制的样式输出
hive> select from_unixtime(1598360886, 'yyyy-MM-dd HH:mm:ss')
, from_unixtime(1598360886, 'yyyy-MM-dd HH')
, from_unixtime(1598360886, 'yyyy-MM-dd')
, from_unixtime(1598360886, 'yyyyMMdd')
OK
  • 日期转化时间戳:unix_timestamp(string date, string format)。

这里同样需要两个参数。

参数1:date为日期字符串,如"2020-01-01"。

参数2:是对应参数 1 的格式,格式必须保持一致,即前面是年月日,这里也要对应年月日。

-- 根据第一个参数格式, 第二个参数格式也需要一致才可以运行
hive> select unix_timestamp('2020-08-23', 'yyyy-MM-dd')
, unix_timestamp('20200823', 'yyyyMMdd')
, unix_timestamp('2020-08-23 21', 'yyyy-MM-dd')
, unix_timestamp('2020-08-23 21:30:30', 'yyyy-MM-dd HH:mm:ss')
, unix_timestamp('20200823 21:30:30', 'yyyyMMdd HH:mm:ss')   --  注意与上一行格式上区别, 以及两个参数对应的调整
OK
1598112000  1598112000  1598112000  1598189430

-- 根据第一个参数格式, 第二个参数格式也需要一致才可以运行
hive> select unix_timestamp('2020-08-23', 'yyyy-MM-dd')
, unix_timestamp('20200823', 'yyyyMMdd')
, unix_timestamp('2020-08-23 21', 'yyyy-MM-dd')
, unix_timestamp('2020-08-23 21:30:30', 'yyyy-MM-dd HH:mm:ss')
, unix_timestamp('20200823 21:30:30', 'yyyyMMdd HH:mm:ss')   --  注意与上一行格式上区别, 以及两个参数对应的调整
OK
1598112000  1598112000  1598112000  1598189430
  • yyyyMMdd 转化为 yyyy-MM-dd。

先说下这种转化的应用场景,yyyyMMdd 格式往往会作为 Hive 日期分区常见样式,但是导出的数据在 Excel 表会被认为是 8 位数字,而不是日期格式。这个时候,我们可以在数据导入 Excel 后再手动转化为 yyyy-MM-dd。还有一种方式是 Hive 输出日期格式就是 yyyy-MM-dd, 这也就是这个日期转化的应用场景。

针对这种日期格式转化,我分享下面两种方法给你。

第一种方法:将 from_unixtime 和 unix_timestamp 结合起来。

hive> select from_unixtime(unix_timestamp('20200823','yyyyMMdd'), 'yyyy-MM-dd')
OK
2020-08-23

hive> select from_unixtime(unix_timestamp('20200823','yyyyMMdd'), 'yyyy-MM-dd')
OK
2020-08-23

第二种方法:字符串函数 substr。

hive> select concat(substr('20200823',1,4),"-",substr('20200823',5,2),"-",substr('20200823',7,2)) as dt;
OK
2020-08-23

hive> select concat(substr('20200823',1,4),"-",substr('20200823',5,2),"-",substr('20200823',7,2)) as dt;
OK
2020-08-23
  • 月份转化季度。即给出 1~12 整数,划分到不同季度,提取月份转化为整数。
hive> select floor((month('2020-08-23')-1)/3)+1                  -- 季度 date=yyyy-MM-dd
, floor((cast(substr('20200823',5,2) as int)-1)/3)+1             -- 季度 date=yyyyMMd
, pmod(datediff('2020-08-23','2012-01-01'),7)         -- 星期几, 0-6, 0表示周日, 表示周六
, weekofyear('2020-08-23')                            -- 一年中第几周
, year('2020-08-23')   -- 取出年的数字部分
, month('2020-08-23')  -- 取出月的数字部分
, day('2020-08-23')  -- 取出日的数字部分
, hour('2020-08-23 21:30:35')  -- 取出日的数字部分
, minute('2020-08-23 21:30:35')  -- 取出日的数字部分
, second('2020-08-23 21:30:35')  -- 取出日的数字部分
OK
3 3 0 34  2020  8 23  21  30  35

hive> select floor((month('2020-08-23')-1)/3)+1                  -- 季度 date=yyyy-MM-dd
, floor((cast(substr('20200823',5,2) as int)-1)/3)+1             -- 季度 date=yyyyMMd
, pmod(datediff('2020-08-23','2012-01-01'),7)         -- 星期几, 0-6, 0表示周日, 表示周六
, weekofyear('2020-08-23')                            -- 一年中第几周
, year('2020-08-23')   -- 取出年的数字部分
, month('2020-08-23')  -- 取出月的数字部分
, day('2020-08-23')  -- 取出日的数字部分
, hour('2020-08-23 21:30:35')  -- 取出日的数字部分
, minute('2020-08-23 21:30:35')  -- 取出日的数字部分
, second('2020-08-23 21:30:35')  -- 取出日的数字部分
OK
3 3 0 34  2020  8 23  21  30  35

日期运算很重要,相对容易理解。下面直接列举函数再附上实例,大家会一目了然。

  • datediff(string enddate, string startdate)函数,用来计算两个日期天数差。

即,返回计算两个参数 enddate-startdate 间隔天数。

例子如下:

hive> select datediff('2020-08-23','2020-08-20')
OK
3

hive> select datediff('2020-08-23','2020-08-20')
OK
3
  • date_add(string startdate, int days)的函数例子如下:
hive> select date_add('2020-08-23',3)   -- 注意这里也可以填写负数, 即返回前几天日期
OK
2020-08-26

hive> select date_add('2020-08-23',3)   -- 注意这里也可以填写负数, 即返回前几天日期
OK
2020-08-26
  • date_sub(string startdate, int days) -- 这个用法和 date_add 相反,只是返回当前日期,并减去几天的日期。
hive> select date_sub('2020-08-23',3)  -- 这里也可以为负数,即返回后几天日期
OK
2020-08-20

hive> select date_sub('2020-08-23',3)  -- 这里也可以为负数,即返回后几天日期
OK
2020-08-20

除了以上处理技巧,我们在第一节数据处理十大技能专门讲了 Hive 两个重要技能:行列转化和 row_number 函数。如果你感兴趣,可以去“入行必备:数据处理的十大技巧”进行学习。

查询性能优化

Hive 查询优化功能表现在很多方面,这里主要讲解日常工作中数据分析师应该注意的 case。

合并小文件

Hive SQL 被解析为 MapReduce 执行,一个文件会对应一个 mapper 来处理。如果数据源是大量的小文件,则会启动大量的 mapper 任务。这就好比一辆卡车一次只运送一个西瓜,浪费了大量资源。这时候,我们需要把西瓜聚合起来,让一辆卡车一次至少搬运几百斤西瓜。

对于 Hive 来说,就是将输入的小文件进行合并,从而减少 mapper 任务数量。

例如,当查询数据时,如果已知对应数据源有很多小文件,可以先进行如下设置:

set hive.input.format=org.apache.hadoop.hive.ql.io.CombineHiveInputFormat; -- Map端输入、合并文件之后按照block的大小分割(默认)
set hive.input.format=org.apache.hadoop.hive.ql.io.HiveInputFormat; -- Map端输入,不合并

当自己在建立中间表时,输出数据为了避免小文件过多,可以进行如下设置:

set hive.merge.mapredfiles = true;
set hive.merge.size.per.task=256000000;  -- 256MB
set hive.merge.smallfiles.avgsize=256000000;  -- 256MB

set hive.merge.mapredfiles = true;
set hive.merge.size.per.task=256000000;  -- 256MB
set hive.merge.smallfiles.avgsize=256000000;  -- 256MB
数据倾斜

数据倾斜是什么?我同样举一个卡车运西瓜的例子说明。假设现在有 1 万个西瓜,有 100 辆卡车,前 99 辆车每辆车一次只运送 1 个西瓜,剩余 9901 个西瓜都让最后一辆卡车拉。这就是“西瓜倾斜”。同样,在大数据处理过程中,一个数据处理任务的数据量可能有几百 GB 或者几十 TB,对应会有几百甚至几千台机器一起处理。如果某一台机器被分配的数据量比其他机器多几十倍甚至几百倍,那就是我们说的数据倾斜了。

这样的情况造成的结果是,数据处理任务结束时间因为其中一台机器而延后,处理效率被严重降低。避免数据倾斜非常重要。前几年数据分析师面试的时候,面试官经常考应聘者数据倾斜的处理问题。现在解决数据倾斜的方案更加成熟,社区考虑 Hive 使用过程中会出现这种情况,进而提供了比较好的解决方案。现在,仅仅需要你配置如下参数分两部分进行。

第一部分:有些聚合可以先在 Map 端进行,然后进入 Reduce 端,这时数据量会小很多。我们再得出最终结果即可。

set hive.map.aggr=true; -- 开启Map端聚合参数设置
set hive.grouby.mapaggr.checkinterval=100000; -- 在Map端进行聚合操作的条目数目

set hive.map.aggr=true; -- 开启Map端聚合参数设置
set hive.grouby.mapaggr.checkinterval=100000; -- 在Map端进行聚合操作的条目数目

第二部分:如下参数将会添加一个 MapReduce 任务,目的就是让数据平均分配到各个 reduce 上,这样基本能解决数据倾斜问题。

set hive.groupby.skewindata = true; -- 有数据倾斜的时候进行负载均衡(默认是false)

set hive.groupby.skewindata = true; -- 有数据倾斜的时候进行负载均衡(默认是false)

上面通过实例解释了 Hive 进行数据统计中,为什么会遇到数据倾斜,以及如何解决数据倾斜。最后我们来分享下 Hive 如何合理控制 map 和 reduce 数量来优化数据处理效率。

控制 map/reduce 任务数量

控制 map 数量

我们先来了解下什么情况要设置 map 数量。一般来讲,map 数量默认,不需要我们设置。Hadoop 运维老师们会监控和设置一个相对通用的值。一般情况下,默认值就能满足需求了。

但是,当我们明确知道表的数据量不大,而 Hive 运行启动了几千个 map 的时候,就有必要减小 map 的数量了。好比 1000 个西瓜没必要安排 100 辆车去拉,安排 2 辆车就可以搞定了。

另一方面,当我们发现 map 数量不多,但 map 运行速度极慢的时候。这时可以 check 下数据表,看看实际需求是不是很大?如果 Hive 启动的 map 数据比较少,就如同用 2 辆车去拉 10000 个西瓜,明显是不够的。

假设如果真遇到上面情况,那么如何调整 map 数量?我们通常会采用以下两种方式解决。

  • 第一种解决办法是增加 mapper 个数。可以设置 set mapred.map.tasks= 一个很大的数值, 需要比系统默认的 map 数量大。
  • 第二种解决办法是减少 mapper 个数。set maperd.min.split.size= 一个数字,该数值单位是字节,比如设置 1GB,即为 1024000000,因为默认一个 mapper 是 64MB,这样设置就可以让一个 mapper 处理 1GB 数据,自然 mapper 的数量也就减少了。

上面介绍 Hive 任务在什么情况下需要调整 map 数量, 以及如何设置 map 数量,下面将对 reducer 数量的控制。

控制 reducer 数量

相比调整 mapper 数量,调整 reducer 数量的场景在工作中相对要多一些。当然,一般情况下我们也是不需要调整的。有些同学认为设置更多 reducer 会加快计算任务,但其实结果却不尽人意。

比如:当数据经过 map 操作后输出的数据结果只有 1KB 大小,却要启动 100 个 reducer 去处理。这时候启动和消耗 reducer 的时间就会很多, 最后会形成 100 个非常小的文件落盘,这对以后的任务计算来讲都是负担。相反,如果 mapper 输出的数据结果很大。有 100 GB,却只有 2 个 reducer 去处理,那必然也会浪费很长时间。这个时候,就需要调节 reducer 数量了。

我们来看如何调整 reducer 数量,这里主要有两种调整方式。

方式一:通过设置每个 reducer 处理的数量大小,最多 reducer 数量来间接控制 reducer 数据。

set hive.exec.reducers.bytes.per.reducer=一个数字(默认1GB, 即1024000000)
set hive.exec.reducers.max=一个数字(默认为999)

set hive.exec.reducers.bytes.per.reducer=一个数字(默认1GB, 即1024000000)
set hive.exec.reducers.max=一个数字(默认为999)

方式二:直接设置 reducer 数量。比如下面设置 reducer 为 500,无论数据大小,都会强制启动 500 个 reducer 处理数据。

set mapred.reduce.tasks=500

上面分享的对 reducer 数量的控制,可能工作中会遇到。尤其在多个大表进行 join 操作的时候,希望你可以掌握使用的场景和方法。

总结

关于 Hive 使用的技巧有很多。这节课给出的技巧主要来自我工作 5 年多的使用经验,挑选出的一些比较有价值的核心处理技巧。我把这些技巧分享给你,这里非常感谢你的学习,之后 Hive 相关问题,可以留言交流学习。


04 晋级之路:轻松掌握新星 Spark 的入门之道

上一课时我们介绍了 Hive 以及常用技能,今天我们来学习下 Spark。因为 Spark 拥有强大的数据处理能力,近几年越来越多的互联网公司开始使用 Spark,掌握 Spark 基础技能,可以大大扩大你匹配数据分析和处理相关工作岗位的机会。

初识 Spark

首先,我们来简单看下 Spark 的背景,它于 2009 年诞生于伯克利大学的 AMPLab,是一款基于内存计算的大数据并行计算框架,被设定为可用于构建一些大型的、低延迟的数据分析的应用程序。

2013 年,Spark 在加入 Apache 孵化器项目后,凭借东风,开始迅速地发展。如今,Spark 已成为 Apache 最重要的三大分布式计算系统开源项目之一(三大分布计算系统为 Hadoop、Spark、Storm)。另一方面,自 2015 年开始, 很多公司部署以 Spark 代替 Hive、MapReduce 的战略。其中 Spark 开发语言 Scala,也因为 Spark 的走火,成为受大厂欢迎的技能。

Spark 的优势所在

通过上节课我们了解到,Hive 任务和许多任务一样,也是被翻译成 MapReduce 后进行大数据处理的。Spark 相比 MapReduce,最大的优势就是:快! 天下武功,唯快不破。Spark 占了“快”这个优点,因此获得了互联网大厂的青睐。

Spark 的快表现在以下两点:

第一,执行速度快。 主要原因是 MR 作业对中间处理的数据需要落盘,下次再读取时。磁盘 I/O 成本高。相对而言,基于内存计算的 Spark 就不存在这个问题。对 Spark 而言,中间数据优先放到集群内存,只有数据量大到内存无法保存时,才会保存到磁盘。另外 MR 作业有多个 job, 每个 job 运行完后,下个 job 都需要重新申请计算资源。而 Spark 作业则会一直持续到数据处理结束,中间不需要再向集群申请计算资源。

下面是 Spark 和 MR 性能比较,我们可以看出,对比 MR,Spark 仅使用了十分之一的计算资源,便获得了比 Hadoop 快 3 倍的速度。

Hive HA hive Hadoop spark先学什么_spark

第二,MR 程序编写的复杂度高。 比如一个基础统计需要几十行程序,Spark 则只需要几行程序,不用像 MR 那样配置各个隐藏在各个函数的操作,简洁高效。它不仅节省人力,而且专注数据逻辑处理。

举个简单的例子。

import java.io.IOException;
import java.util.StringTokenizer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
public class WordCount {
  public static class TokenizerMapper
       extends Mapper<Object, Text, Text, IntWritable>{
    private final static IntWritable one = new IntWritable(1);
    private Text word = new Text();
    public void map(Object key, Text value, Context context
                    ) throws IOException, InterruptedException {
      StringTokenizer itr = new StringTokenizer(value.toString());
      while (itr.hasMoreTokens()) {
        word.set(itr.nextToken());
        context.write(word, one);
      }
    }
  }
  public static class IntSumReducer
       extends Reducer<Text,IntWritable,Text,IntWritable> {
    private IntWritable result = new IntWritable();
    public void reduce(Text key, Iterable<IntWritable> values,
                       Context context
                       ) throws IOException, InterruptedException {
      int sum = 0;
      for (IntWritable val : values) {
        sum += val.get();
      }
      result.set(sum);
      context.write(key, result);
    }
  }
  public static void main(String[] args) throws Exception {
    Configuration conf = new Configuration();
    Job job = Job.getInstance(conf, "word count");
    job.setJarByClass(WordCount.class);
    job.setMapperClass(TokenizerMapper.class);
    job.setCombinerClass(IntSumReducer.class);
    job.setReducerClass(IntSumReducer.class);
    job.setOutputKeyClass(Text.class);
    job.setOutputValueClass(IntWritable.class);
    FileInputFormat.addInputPath(job, new Path(args[0]));
    FileOutputFormat.setOutputPath(job, new Path(args[1]));
    System.exit(job.waitForCompletion(true) ? 0 : 1);
  }
}

import java.io.IOException;
import java.util.StringTokenizer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
public class WordCount {
  public static class TokenizerMapper
       extends Mapper<Object, Text, Text, IntWritable>{
    private final static IntWritable one = new IntWritable(1);
    private Text word = new Text();
    public void map(Object key, Text value, Context context
                    ) throws IOException, InterruptedException {
      StringTokenizer itr = new StringTokenizer(value.toString());
      while (itr.hasMoreTokens()) {
        word.set(itr.nextToken());
        context.write(word, one);
      }
    }
  }
  public static class IntSumReducer
       extends Reducer<Text,IntWritable,Text,IntWritable> {
    private IntWritable result = new IntWritable();
    public void reduce(Text key, Iterable<IntWritable> values,
                       Context context
                       ) throws IOException, InterruptedException {
      int sum = 0;
      for (IntWritable val : values) {
        sum += val.get();
      }
      result.set(sum);
      context.write(key, result);
    }
  }
  public static void main(String[] args) throws Exception {
    Configuration conf = new Configuration();
    Job job = Job.getInstance(conf, "word count");
    job.setJarByClass(WordCount.class);
    job.setMapperClass(TokenizerMapper.class);
    job.setCombinerClass(IntSumReducer.class);
    job.setReducerClass(IntSumReducer.class);
    job.setOutputKeyClass(Text.class);
    job.setOutputValueClass(IntWritable.class);
    FileInputFormat.addInputPath(job, new Path(args[0]));
    FileOutputFormat.setOutputPath(job, new Path(args[1]));
    System.exit(job.waitForCompletion(true) ? 0 : 1);
  }
}
  • Spark 进行 WordCount 统计
val lines = sc.textFile("hdfs路径或本地文件路径")
val res = lines.flatMap(line=> line.split(" ")) \
             .map(word=> (word, 1)) \
             .reduceByKey(_ + _)
res.saveAsTextFile("hdfs://...或本地文件路径")

通过代码对比,我们明显能看出,Spark 比 Java 简洁不止一两行代码。

MR 只有 map 和 reduce 两种操作,稍复杂的统计逻辑,就需要通过两种操作组合成多个 job 才能完成,另外还需要复杂的参数配置,最后使程序变得十分臃肿,开发成本很高。 Spark 提供多种转化操作方法,除了 map 和 reduce,还包括 flatMap、filter、union、join、reduceByKey、 sample、zip 等,这使得数据处理代码变得极其简洁。

在互联网公司,时间就是成本,速度在某种程度上限制业务增长的瓶颈,这是老板最不想看到的。Spark 在大数据处理上的优势,不仅能够让数据流动得更快,而且因其语言上的简洁,更加提升了工程师开发效率,让数据挖掘的成本变得更低。大厂积极部署 Spark,反哺 Spark 开源社区,也使得 Spark 开源社区极其活跃,Spark 数据生态也越来越完善。

Spark 数据生态完善

除了数据处理性能提升,Spark 还提供完善的大数据处理库套装,包括:Spark SQL、Spark Streaming(流式计算)、MLlib(机器学习)、GraphX (图计算)等, 这些功能也让 Spark 相比 Hadoop 优势更大了。

  • Spark Core:Spark Core 包含 Spark 的基本功能,如内存计算、任务调度、部署模式、故障恢复、存储管理等。
  • Spark SQL:Spark SQL 允许开发人员直接处理 RDD,同时也可查询 Hive、 HBase 等外部数据源。 Spark SQL 的一个重要特点是其能够统一处理关系表和 RDD,使得开发人员可以轻松地使用 SQL 命令进行查询,并进行更复杂的数据分析。
  • Spark Streaming:Spark Streaming 支持高吞吐量、可容错处理的实时流数据处理,其核心思路是将流式计算分解成一系列短小的批处理作业。
  • MLlib(机器学习):MLlib 提供了常用机器学习算法的实现,包括聚类、分类、回归、协同过滤等。从而降低了机器学习的门槛,开发人员只要具备一定的理论知识就能进行机器学习的工作。
  • GraphX(图计算):GraphX 是 Spark 中用于图计算的 API ,可认为是 Pregel 在 Spark 上的重写及优化,Graphx 性能良好,拥有丰富的功能和运算符,能在海量数据上自如地运行复杂的图算法。

以上完善了 Spark 数据生态,让开发者通过 Spark 能挖掘更多数据价值,这也使 Spark 在同类产品中脱颖而出。

Spark 应用场景

Spark 是一款基于内存的迭代计算框架,适用于需要多次操作特定数据集的应用场合。需要反复操作的次数越多,所需读取的数据量越大,受益越大,数据量小但是计算密集度较大的场合,受益就相对较小。

Spark 两类应用场景:离线统计实时计算

  • 离线统计

Spark 会应用在一些更底层的更大数据量离线任务中,在相同计算资源条件下,Spark 数据处理性能可以保证报表数据准时生产。

举个例子,亿级日活用户的 App,每个用户使用 App 的产生的数据,包括浏览的内容、转评赞行为等都会形成日志上传。这部分用户的行为数据,需要经过许多层的数据任务处理才能最后呈现到数据分析师、产品、运营等业务方眼前。这时候,保证昨天的数据能被今天业务方及时看到,并推动各自业务制定决策。就显得尤为重要了。传统 Hive 遇到数据量较大的情况,处理效率会变低,这样就使得重要的数据指标延迟很久。昨天的数据有可能要今天下午、晚上甚至第 3 天才看到,严重影响业务决策。

这时候 Spark 的优势就体现出来了。首先,使用 Spark 处理最底层日志的前几次统计聚合。如果聚合后数据量相对较小,再融合 Hive 数据任务进行处理。这样就能大幅提升数据产出效率,让数据能够及时为业务方所使用了。

  • 实时计算

除了数据处理和统计,Spark 主要应用在互联网计算广告、准实时报表、机器学习等业务上。我们来看一下 Spark 在广告业务准实时报表方面的运用。

广告业务:广告业务作为最依赖算法模型的业务,需要大量的互联网模型训练和预估,中间经过无数数据迭代,一份数据会被切分成很多部分,经历过很多轮训练。这时候 Spark 以最快速度完成模型训练的优势就能体现出来了。

准实时报表: 业务方希望实时掌控最近每分钟,App 各个功能模块被使用的次数,即沿着时间轴,按每分钟被切分成一段段,希望了解每分钟产生的各维度指标的统计。这个 Hive 完全无法完成。Spark Streaming 是专门为此场景设计的流式计算解决方案之一。

Spark 专用编程语言 Scala

Spark 使用 Scala 语言进行实现。这里简单介绍下 Scala 语言。Scala 来源于 Java,但 Scala使用函数式编程思维来开发程序。 另外 Scala 程序可以编译成 .class 文件在 JVM 上运行。这点与 Java 相同,这两种语言大部分 API 是可以相互调用的。

Scala 学习门槛相比 Java 来说要高一些。这里主要介绍与 Scala 的数据处理相关的函数和方法,目的在于带你了解 Scala 基本用法,从而方便学习 Spark 数据处理部分。对于 Scala 高阶函数使用方法感兴趣的同学,可以参考本文最后推荐的网站进行学习。

表达式

我们几乎可以说 Scala 一切都是表达式。

scala> 1+2
res1: Int = 3

scala> 1+2
res1: Int = 3

res1 是解释器自动创建的变量名称,用来指代表达式的计算结果。它是 Int 类型,值为3。

变量类型

Scala 的变量类型只有两种类型:valvar。val 为不可变变量,我们来看下面的例子:

// 声明变量a, 可以将任何数据或者表达式赋值给a
scala> val a = 100
a: Int = 100
scala> val a = "hello scala"
a: String = hello scala
scala> val a = 1 + 2
a: Int = 3
// 但不能对a再进行修改
scala> a = 1
<console>:25: error: reassignment to val
       a = 1
         ^

var 为可变变量,即声明后可以再次修改的变量, 举例:

// 声明a为可变变量
scala> var a = 100
a: Int = 100
// a的值, 可以被修改
scala> a = 99
a: Int = 99
// 但注意, a的类型, 不能被修改
scala> a = "hello"
<console>:25: error: type mismatch;
 found   : String("hello")
 required: Int
       a = "hello"
           ^

注意:在数据处理过程中,能使用 val 的时候,尽量不使用 var。这是为了让多线程中的并发访问保持读写一致性。可变变量的值可以被其他逻辑修改,常量则不能被修改。因此整体上更为稳定一些。

数据类型

Scala 的基础数据类型,如表格所示,主要包括 Byte、Short、Int、Long、Float、Double、Char、String、Boolean、Unit、Nul、Nothing 等。

Hive HA hive Hadoop spark先学什么_数据分析_02

基础数据结构

Scala 基础数据结构还包括:数组、列表、元组、集合,和其他高级语言类似。简单举个例子:

// 数组
val arr = new Array[Int](3)
scala> val arr = new Array[Int](3)
arr: Array[Int] = Array(0, 0, 0)
// 给数组每个元素, 分别赋值(默认为0)
scala> arr(0) = 10
scala> arr(1) = 20
scala> arr(2) = 30
scala> arr
res5: Array[Int] = Array(10, 20, 30)
// 列表
scala> val list = List(10,20,30)
list: List[Int] = List(10, 20, 30)
scala> list.sum
res10: Int = 60
// 元组
scala> val tuple = ('a', 100, 'b', 99)
tuple: (Char, Int, Char, Int) = (a,100,b,99)
// 集合
scala> var set = Set("wang", "li", "zhang")
set: scala.collection.immutable.Set[String] = Set(wang, li, zhang)
// 增加重复的元素"li", 结果不变
scala> set+="li"
scala> set
res7: scala.collection.immutable.Set[String] = Set(wang, li, zhang)
// 增加不同元素, 会体现出来
scala> set+="sun"
scala> set
res9: scala.collection.immutable.Set[String] = Set(wang, li, zhang, sun)

以上介绍了 Scala 的基础变量、数据类型和数据结构。其实函数方法调用对于每种数据结构都集成了,这里就不细说了。你可以在交互式命令行进行手敲学习,多实践几次就熟练了。
有了对 Scala 语言基础认知,我们下面再开始介绍 Spark 如何进行数据处理的。

Spark 数据处理实例

Spark 的核心是建立在统一的抽象 RDD (Resilient Distributed Dataset) 之上,RDD 全称为弹性分布式数据集。 RDD 是一个不可变、可分区、里面元素可以并行计算的集合。它使 Spark 的各个组件可以无缝进行集成(现在 Spark 3.0 之后开始倡导 DataSet ,但理解 RDD 能够帮助我们更好地理解 Spark)。

Spark 数据处理的基本流程:第一步,创建 RDD;第二步,对 RDD 进行数据处理,并最终生成想要的数据结果。

创建 RDD

我们可以从本机或者集群获取数据创建 RDD 。

第一,从本机创建 RDD。参考下面代码:

// sc 是Spark交互界面自带的 sparkContext, 它有所有spark内部函数可调用
scala> val lines = sc.textFile("file:///usr/local/.../word.txt")

// sc 是Spark交互界面自带的 sparkContext, 它有所有spark内部函数可调用
scala> val lines = sc.textFile("file:///usr/local/.../word.txt")

第二,从集群创建 RDD。以下代码进行了说明。

scala> val lines = sc.textFile("hdfs:///usr/xxx/.../word.txt")

scala> val lines = sc.textFile("hdfs:///usr/xxx/.../word.txt")

第三,通过数组、集合、列表等创建 RDD。可以参考一下代码。

// 声明一个列表变量
scala> val list = List(10, 20, 30)
list: List[Int] = List(10, 20, 30)
// 通过函数parallelize将列表转化成RDD
scala> val rdd = sc.parallelize(list)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[18] at parallelize at <console>:26

// 声明一个列表变量
scala> val list = List(10, 20, 30)
list: List[Int] = List(10, 20, 30)
// 通过函数parallelize将列表转化成RDD
scala> val rdd = sc.parallelize(list)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[18] at parallelize at <console>:26
RDD 转换 ( transformations ) 和行动 ( actions )

详细可参考:spark 官网:rdd-programming-guide

transformations 常用的 API 有以下形式:

  • map(func):通过 func 函数,对 RDD 数据转换生成新 RDD,可以简单理解为 Python 中 lambda 函数。
  • filter(func):对于原有 RDD 中,满足经过 func 处理后返回 True 的数据保留下来,生成新数据集合。
  • flatMap(func):将原有数据打平,可以简单理解为行转列。
  • sample(withReplacement, fraction, seed):对原有 RDD 抽样,生成新 RDD
    union(otherDataset): 将两个 RDD 合并后生成新 RDD。
  • reduceByKey(func, [numPartitions]):针对( K, V )样式 RDD,将 K 作为被聚合的 key,计算 V 的值。
  • repartition(numPartitions):将原有 RDD 重新组合到不同的分区中,比如原来 RDD 在 1000 个分区上,小文件过多,但实际只有几 MB 数据,这时一般会 reshuffle 到一个分区即可。

actions 常用 API 主要有以下形式:

  • reduce(func):使用函数来聚合 RDD 相关的元素。
  • collect():将数据集的所有元素作为数组返回。
  • null
    count():返回 RDD 元素数量。
  • first():取 RDD 第一个元素。
  • take(n):取 RDD 第 N 个元素。
  • saveAsTextFile(path):将处理后的 RDD 保存到 HDFS 路径。
  • foreach(println):对 RDD 中每个元素,使用 func 进行处理,比如打印每个元素。

RDD 转换操作,即 transformations 操作,是指将 RDD 由一种数据结构样式转换成另外一种样式,每次转换会生成新的 RDD 供下次 transformations 使用。需要注意的是,通过transformations 生成的 RDD 是惰性求值的,可以简单理解为一个 RDD 经过各种transformations ,只是记录了数据转换的过程,不会触发计算。只有最终遇到 actions 操作,才会发送真正的计算。

接下来我们通过实际案例,了解下 transformations 和 actions 函数的使用。

数据源介绍
// 
scala> val rdd = sc.textFile("/Users/leihao1/data/qujinger/专栏课程/example-city")
// take, foreach用法举例:打印前3行 
scala> rdd.take(3).foreach(println)
2019/8/14	浙江省	861225
2019/10/24	广东省	767495
2020/3/18	河北省	767495

数据源中共三列,分别是:日期、省份、用户访问次数。下面通过 Spark 来完成下面几个统计任务:

  1. 数据总行数。
  2. 各省份的访问次数总和。
  3. 过滤出大于 50 万次访问次数的省份。

这些都是简单而普遍的统计场景,我们看看 Spark 是如何完成的。

第一步:读取数据。
Spark 在进行数据处理前,也需要先加载数据,可以从本机读取数据,也可以从分布时集群获取数据。

下面演示的是读取本地的练习数据。如果希望读 HDFS 数据,只需要将本地路径替换成 HDFS 路径即可。

// map用法举例: 通过map将原来rdd转化为另外一个rdd,只取后两列数据
scala> val rdd1 = data.map(line => line.split("\t")).map(arr => (arr(1), arr(2)))
rdd1: org.apache.spark.rdd.RDD[(String, String)] = MapPartitionsRDD[26] at map at <console>:27
scala> rdd1.take(3).foreach(println)
(浙江省,861225)
(广东省,767495)
(河北省,7674)

第二步:行数统计
数据分析师最常用聚合函数完成数据统计。比如,查询一个表的总函数,计算某个字段数据之和。Spark 也有类似的基础功能。下面,我们通过 Spark 来统计数据源的总行数,以及 pv 字段的加和,也就是看总的浏览次数。

// count用法举例:计算行数
scala> rdd1.count
res33: Long = 20
// reduce用法举例:计算总pv数, 即将最后一列数字求和
scala> rdd1.map{case(city, pv) => pv.toLong}.reduce(_+_)
res37: Long = 7806

第三步:分组聚合。
Spark 分组聚合函数是用 reduceByKey 实现的,类似 SQL 语言里的 group by 用法,但又远比 SQL 功能强大。下面我们通过 reduceByKey 来分组汇总下各个省份的总流量。

// reduceByKey用法举例:统计各省份的总行数, 总pv数
scala> rdd1.map{case(province, pv) =>
     | (province, (1, pv.toLong))
     | }.reduceByKey{case((n1, pv1), (n2, pv2)) =>
     | (n1+n2, pv1+pv2)
     | }.foreach(println)
(湖北省,(1,541376))
(江西省,(1,371881))
(浙江省,(1,861225))
(湖南省,(1,79023))
(江苏省,(1,371881))
(华盛顿,(1,371881))
(重庆,(1,371881))
(山东省,(1,541376))
(河北省,(1,767495))
(广东省,(4,1759775))
(上海,(6,1396543))
(河南省,(1,3718

第四步:完成过滤
数据分析师在查询数据的时候,都会用到条件过滤来获取满足自己查询目标,即 SQL 语言中的 where 条件语句。在 Spark 中的过滤方式有很多种, 其中最常用的是 filter 函数。下面我们,通过 filter 函数来过滤出满足 50 万浏览量的数据行:

// filter用法举例:过滤函数
scala> rdd1.filter{case(_, pv) => pv.toLong>=500000}.foreach(println)
(浙江省,861225)
(广东省,767495)
(河北省,767495)
(山东省,541376)
(广东省,541376)
(上海,541376)
(湖北省,541376)

以上,通过 Spark 来完成数据分析师熟悉的几个统计 case ,相信你会对 Spark 数据处理有更直观的认识。在实际工作中,Spark 处理的数据量和复杂度可能会更高。在提交 Spark 任务时,肯定会涉及基础资源配置、内存调优、数据倾斜处理等问题,这里不展开讲了,我们只需要了解,Spark 基本上就是按照上面方式,再进行组合和优化,完成基础数据处理的。

总结

本课时先介绍到这里,希望通过学习,能让你了解到 Spark 的优势,使用场景,以及如何使用 Spark 进行大数据处理。非常感谢你的学习,如果有关于 Spark 数据处理的一些问题,欢迎在留言区提问。

最后给你推荐一些学习网站。Spark 生态很强大,对于数据分析师来讲,可以重点了解 Spark 数据处理的部分。对机器学习、流失计算感兴趣的同学,则可以进一步学习 MLlib、Spark Streaming。而官网是最好最全面的学习文档,感兴趣的同学可以点击以下链接,通过网站进行进一步学习。