[PySpark学习]RDD的重要算子

一、RDD概念

RDD(英文全称Resilient Distributed Dataset),即弹性分布式数据集是spark中引入的一个数据结构,是Spark中最基本的数据抽象,代表一个不可变、可分区、里面的元素可并行计算的集合。

Resilient弹性:RDD的数据可以存储在内存或者磁盘当中,RDD的数据可以分区。

Distributed分布式:RDD的数据可以分布式存储,可以进行并行计算。

Dataset数据集:一个用于存放数据的集合。

在上一篇《SparkRDD的转换与动作算子》中,介绍了SparkRDD中的一些常用算子,下面介绍RDD中的一些重要算子,是在实际是工作用的比较多,而且比较复杂的一些算子。

以下演示通过SecureCRTPortable客户端远程连接Linux服务器操作pyspark。

        连接界面如下:

二、分区算子

作用:针对整个分区数据进行处理的算子。

1、mapPartitions算子

        输入数据:rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],3),先查看分区情况

>>> rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],3)

# 查看分区
>>> rdd.glom().collect()     

# 运行结果 [[1, 2, 3], [4, 5, 6], [7, 8, 9, 10]]  

        解释:parallelize(data,num)通过自定义列表的方式初始化RDD对象。(一般用于测试)

                        其中data为输入的数据,num为选择分区数。

                   glom()  可以将RDD以分区形式输出,查看数据在哪个分区。

                   getNumPartitions() 查看当前RDD有多少个分区

        目前parallelize设置了3个分区,因此输出结果有3个列表。

        以下通过map和mapPartitions的对比查看两个算子的区别:

        需求: 对数字加一

        map效果代码:

>>> rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],3)
>>> 
#自定义函数
>>> def my_add(num):
...     print(f"传递进来的数据{num}")
...     return num+1
... 
>>> rdd.map(my_add).collect()


###  运行结果
传递进来的数据4
传递进来的数据5
传递进来的数据6
传递进来的数据1
传递进来的数据2
传递进来的数据3
传递进来的数据7
传递进来的数据8
传递进来的数据9
传递进来的数据10
[2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

        总结:map被调用了10次,反复操作会导致资源消耗,浪费资源。       

         mapPartitions效果代码:

>>> rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],3)
# 自定义函数
>>> def my_add(list):
...     print("输入的参数",list)
...     
...     new_list = []
...     
...     for i in list:
...             new_list.append(i + 1)
...     return new_list
... 
>>> rdd.mapPartitions(my_add).collect()

### 运行结果
输入的参数 <itertools.chain object at 0x7f96e319c940>
输入的参数 <itertools.chain object at 0x7f96e319c940>
输入的参数 <itertools.chain object at 0x7f96e3375e50>
[2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

        总结:可以看出使用mapPartitions分区算子只调用了3次资源,减少了大量的重复操作,节省资源。

2、foreachPartition算子

        以下通过foreach和foreachPartition的对比查看两个算子的区别:

        需求: 遍历打印

        foreach效果代码:

>>> rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],3)
# 自定义函数
>>> def my_print(num):
...     print(f"传递进来的数据{num}")
...     print(num)
... 
>>> rdd.foreach(my_print)

### 运行结果
传递进来的数据4
4
传递进来的数据5
5
传递进来的数据6
6
传递进来的数据1
1
传递进来的数据2
2
传递进来的数据3
3
传递进来的数据7
7
传递进来的数据8
8
传递进来的数据9
9
传递进来的数据10
10

        foreachPartition效果代码:

>>> rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],3) 
# 自定义函数
>>> def my_print(list):
...     print(f"传递进来的数据{list}")
...     
...     for i in list:
...             print(i)
... 
>>> rdd.foreachPartition(my_print)

### 运行结果
传递进来的数据<itertools.chain object at 0x7f96e319c2b0>
4
5
6
传递进来的数据<itertools.chain object at 0x7f96e319c2b0>
1
2
3
传递进来的数据<itertools.chain object at 0x7f96e3375a60>
7
8
9
10

mapPartitions和foreachPartition分区算子总结

        1、 map和foreach算子都有对应的分区算子,分别是mapPartitions和foreachPartition

        2、 分区算子适用于有反复消耗资源的操作,例如:文件的打开和关闭、数据库的连接和关闭等,能够减少操作的次数。

        3、 如果没有反复消耗资源的操作,调用两类算子,效果一样。

三、重分区算子

作用:对RDD的分区重新进行分区操作的算子,也就是改变RDD分区数的算子。

1、repartition算子

        格式:repartition(num)

        作用:改变RDD分区数。既能够增大RDD分区数,也能够减小RDD分区数。但是都会导致发生Shuffle过程。

        需求:增大与减小分区

        代码:

>>> rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],3)
# 查看分区
>>> rdd.glom().collect()
[[1, 2, 3], [4, 5, 6], [7, 8, 9, 10]]

# 增大分区
>>> rdd.repartition(5).glom().collect()
[[], [1, 2, 3], [7, 8, 9, 10], [4, 5, 6], []]     

# 减小分区                              
>>> rdd.repartition(2).glom().collect()
[[1, 2, 3, 7, 8, 9, 10], [4, 5, 6]]

2、coalesce算子

        格式:coalesce(num,shuffle=True|False)

        作用:改变RDD分区数。但是,默认只能减小RDD分区数,不能增大,减小过程中不会发生Shuffle过程。如果想增大分区,需要将参数shuffle设置为True,但是会导致Shuffle过程。

        需求:减小再增大分区

        代码:

>>> rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],3)
# 查看分区
>>> rdd.glom().collect()
[[1, 2, 3], [4, 5, 6], [7, 8, 9, 10]]

# 减小分区
>>> rdd.coalesce(2).glom().collect()
[[1, 2, 3], [4, 5, 6, 7, 8, 9, 10]]

# 增大分区
>>> rdd.coalesce(5).glom().collect()
[[1, 2, 3], [4, 5, 6], [7, 8, 9, 10]]

        需求:将参数2设置为True,再增大与减少分区

        代码:

>>> rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],3)

# 将参数2设置为True,再增大分区
>>> rdd.coalesce(5,shuffle=True).glom().collect()
[[], [1, 2, 3], [7, 8, 9, 10], [4, 5, 6], []]

# 将参数2设置为True,再减小分区
>>> rdd.coalesce(2,shuffle=True).glom().collect()
[[1, 2, 3, 7, 8, 9, 10], [4, 5, 6]]

repartition 和 coalesce总结

        1、 这两个算子都是用来改变RDD的分区数。

        2、 repartition 既能够增大RDD分区数,也能够减小RDD分区数。但是都会导致发生Shuffle过程。

        3、 默认只能减小RDD分区数,不能增大,减小过程中不会发生Shuffle过程。如果想增大分区,需要将参数shuffle设置为True,但是会导致Shuffle过程。

        4、 repartition 底层实际上是调用了coalesce算子,并且将shuffle参数设置为了True。

3、partitionBy算子

        格式:partitionBy(num,[fn])

        作用:该算子主要是用来改变key-value键值对数据类型RDD的分区数的。num表示要设置的分区数;fn参数是可选,用来让用户自定义分区规则。        

        代码:

>>> rdd = sc.parallelize([(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(10,10)],5)

# 查看分区
>>> rdd.glom().collect()
[[(1, 1), (2, 2)], [(3, 3), (4, 4)], [(5, 5), (6, 6)], [(7, 7), (8, 8)], [(9, 9), (10, 10)]]

# 增大分区,尝试分为20个分区
>>> rdd.partitionBy(20).glom().collect()
[[], [(1, 1)], [(2, 2)], [(3, 3)], [(4, 4)], [(5, 5)], [(6, 6)], [(7, 7)], [(8, 8)], [(9, 9)], [(10, 10)], [], [], [], [], [], [], [], [], []]

# 减少分区,尝试分为2个分区
>>> rdd.partitionBy(2).glom().collect()
[[(2, 2), (4, 4), (6, 6), (8, 8), (10, 10)], [(1, 1), (3, 3), (5, 5), (7, 7), (9, 9)]]

# 将 key>5 放置在一个分区,剩余放置到另一个分区
>>> rdd.partitionBy(2,partitionFunc=lambda key:0 if key > 5 else 1).glom().collect() 
[[(6, 6), (7, 7), (8, 8), (9, 9), (10, 10)], [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]]
# 注意: 分区编号的数据类型需要是int类型

        总结:

        默认情况下,根据key进行Hash取模分区。如果对默认分区规则不满意,可以传递参数fn来自定义分区规则。但是自定义分区规则函数需要满足两个条件,条件一:分区编号的数据类型需要是int类型;条件二:传递给自定义分区函数的参数是key。

四、聚合算子

(一)单值类型的聚合算子

        1、reduce(fn1)

                作用:根据传入函数对数据进行聚合处理

                需求:求和计算, 求所有数据之和

                代码:

>>> rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],3)

# 查看分区情况
>>> rdd.glom().collect()
[[1, 2, 3], [4, 5, 6], [7, 8, 9, 10]]

# 自定义函数
>>> def my_sum(agg,curr):
...     return agg+curr
... 
>>> rdd.reduce(my_sum)

# 运行结果
55

        2、fold(defaultAgg,fn1)

                作业:根据传入函数对数据进行聚合处理,同时支持给agg设置初始值。

                需求:求和计算, 求所有数据之和

                代码:

>>> rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],3)
# 查看分区情况
>>> rdd.glom().collect()
[[1, 2, 3], [4, 5, 6], [7, 8, 9, 10]]

# 自定义函数
>>> def my_sum(agg,curr):
...     return agg+curr
... 
>>> rdd.fold(5,my_sum)

# 运行结果
75

       

         3、aggregate(defaultAgg, fn1, fn2)

                作业:根据传入函数对数据进行聚合处理。defaultAgg设置agg的初始值,fn1对各个分区内的数据进行聚合计算,fn2 负责将各个分区的聚合结果进行汇总聚合。

                需求:求和计算, 求所有数据之和

                代码:

>>> rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],3)
# 查看分区情况
>>> rdd.glom().collect()
[[1, 2, 3], [4, 5, 6], [7, 8, 9, 10]]

# 自定义函数
>>> def my_sum_1(agg,curr):
...     return agg+curr
... 
>>> def my_sum_2(agg,curr):
...     return agg+curr
... 
>>> rdd.aggregate(5,my_sum_1,my_sum_2)

# 运行结果
75

        单值类型的聚合算子总结

                reduce、fold、aggregate算子都能实现聚合操作。reduce的底层是fold,fold底层是aggregate。在工作中,如果能够通过reduce实现的,就优先选择reduce;否则选择fold,实在不行就选择aggregate。

(二)KV类型的聚合函数

        相关的算子

                1、reduceByKey(fn1)

                2、foldByKey(defaultAgg, fn1)

                3、aggregateByKey(defaultAgg, fn1, fn2);

                以上三个与单值是一样的,只是在单值的基础上加了分组的操作而已,针对每个分组内的数据进行聚合而已。

                另外有一个:groupByKey() 仅分组,不聚合统计。

        问题:groupByKey() + 聚合操作 和 reduceByKey() 都可以完成分组聚合统计,谁的效率更高一些?

       答: reduceByKey(),因为底层会进行局部的聚合操作,会减小后续处理的数据量。

        reduceByKey:

        groupByKey:

五、关联算子

        关联函数,主要是针对kv类型的数据,根据key进行关联操作

        相关的算子:

                1、join:实现两个RDD的join关联操作

                2、leftOuterJoin:实现两个RDD的左关联操作

                3、rightOuterJoin:实现两个RDD的右关联操作

                4、fullOuterJoin:实现两个RDD的满外(全外)关联操作

        以上这些关联算子的作用跟HiveSQL是一样的。

        

  • 23
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值