RDD算子介绍

本文详细介绍了Spark中的RDD算子,包括转换算子和动作算子。转换算子如map、groupBy、filter、flatMap等,它们是惰性的,只有遇到动作算子如collect、reduce、first、take等时才会触发执行。文章还提到了RDD的重要算子如partitionBy、repartition、coalesce等,以及聚合算子reduce、fold和aggregate,以及不同类型的关联操作join、leftOuterJoin、rightOuterJoin和fullOuterJoin。

目录

一.RDD的相关算子

二.RDD算子的分类

三.RDD的转换算子

3.1 (单)值类型算子

3.2 双值类型算子

3.3 key-value数据类型算子

四.RDD的动作算子

五.RDD的重要算子

5.1 基本算子

5.2 分区算子

5.3 重分区算子

5.4 聚合算子

5.5关联算子


一.RDD的相关算子

RDD算子: 指的是RDD对象中提供了非常多的具有特殊功能的函数, 我们将这些函数称为算子 (大白话:指的RDD的API)

相关的算子的官方文档: Spark Core — PySpark 3.1.2 documentation

二.RDD算子的分类

整个RDD算子, 共分为两大类:Transformation转换算子和Action动作算子

Transformation转换算子:
    1- 所有的转换算子在执行完成后,都会返回一个新的RDD
    2- 所有的转换算子都是【Lazy惰性】,并不会立即执行,仅仅是在定义计算的规则
    3- 转换算子必须遇到【Action动作算子】才会触发执行

Action动作算子:
    1- 动作算子在执行后,不会返回一个RDD。【要么没有返回值,要么返回的数据类型不是RDD】
    2- 动作算子都是【立即执行】的,一个动作算子就会产生一个Job的任务,并且会执行这个动作算子所依赖的其他所有RDD

相关的转换算子

相关的动作算子:

三.RDD的转换算子

3.1 (单)值类型算子

  • map算子:

    • 格式: rdd.map(fn)

    • 说明: 主要根据传入的函数,对数据进行一对一的转换操作,传入一行,返回一行

输入: init_rdd = sc.parallelize([0,1,2,3,4,5,6,7,8,9])
需求: 数字加一后返回
代码: init_rdd.map(lambda num:num+1).collect()
结果: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

  • groupBy 算子:

    • 格式: rdd.groupBy(fn)

    • 说明: 根据传入的函数对数据进行分组操作

输入: init_rdd = sc.parallelize([0,1,2,3,4,5,6,7,8,9])
需求: 将数据分成奇数和偶数
代码: init_rdd.groupBy(lambda num:'o' if num%2==0 else 'j').mapValues(list).collect()
结果: [('j', [1, 3, 5, 7, 9]), ('o', [0, 2, 4, 6, 8])]
总结: mapValues(list)将kv数据类型中value转换成列表

  • filter算子:

    • 格式:rdd.filter(fn)

    • 说明:过滤算子,可以根据函数中指定的过滤条件,对数据进行过滤操作,条件返回True表示保留,返回False 表示过滤

输入: init_rdd = sc.parallelize([0,1,2,3,4,5,6,7,8,9])
需求: 过滤掉数值<=3的数据
代码: init_rdd.filter(lambda num:num>3).collect()
结果: [4, 5, 6, 7, 8, 9]

  • flatMap算子:

    • 格式:rdd.flatMap(fn)

    • 说明:在map算子的基础上,加入一个压扁的操作, 主要适用于一行中包含多个内容的操作,实现一转多的操作

输入: init_rdd = sc.parallelize(['张三 李四 王五','赵六 周日'])
需求: 将姓名一个一个的输出
代码: init_rdd.flatMap(lambda content:content.split()).collect()
结果: ['张三', '李四', '王五', '赵六', '周日']

3.2 双值类型算子

  • union(并集) 和intersection(交集)

    • 格式: rdd1.union(rdd2) rdd1.intersection(rdd2)

输入: rdd1 = sc.parallelize([3,2,6,8,0])
     rdd2 = sc.parallelize([3,2,1,5,7])
​
并集: rdd1.union(rdd2).collect()
结果: [3, 2, 6, 8, 0, 3, 2, 1, 5, 7]
​
对并集的结果进行去重: rdd1.union(rdd2).distinct().collect()
结果: [8, 0, 1, 5, 2, 6, 3, 7]
​
交集: rdd1.intersection(rdd2).collect()
结果: [2, 3]

3.3 key-value数据类型算子

  • groupByKey()

    • 格式: rdd.groupByKey()

    • 说明: 根据key进行分组操作

输入: rdd = sc.parallelize([('c01','张三'),('c02','李四'),('c02','王五'),('c01','赵六'),('c03','田七'),('c03','周八'),('c02','李九')])
需求: 对学生按照班级分组统计
代码: rdd.groupByKey().mapValues(list).collect()
结果: [('c01', ['张三', '赵六']), ('c02', ['李四', '王五', '李九']), ('c03', ['田七', '周八'])]
  • reduceByKey()

    • 格式: rdd.reduceByKey(fn)

    • 说明: 根据key进行分组,将一个组内的value数据放置到一个列表中,对这个列表基于fn进行聚合计算操作

输入: rdd = sc.parallelize([('c01','张三'),('c02','李四'),('c02','王五'),('c01','赵六'),('c03','田七'),('c03','周八'),('c02','李九')])
需求: 统计每个班级学生人数
代码: rdd.map(lambda tup:(tup[0],1)).reduceByKey(lambda agg,curr:agg+curr).collect()
结果: [('c01', 2), ('c02', 3), ('c03', 2)]
  • sortByKey()算子:

    • 格式:rdd.sortByKey(ascending=True|False)

    • 说明: 根据key进行排序操作,默认按照key进行升序排序,如果需要降序,设置 ascending 参数的值为False

输入: rdd = sc.parallelize([(10,2),(15,3),(8,4),(7,4),(2,4),(12,4)])
需求: 根据key进行排序操作,演示升序
代码: rdd.sortByKey().collect()
结果: [(2, 4), (7, 4), (8, 4), (10, 2), (12, 4), (15, 3)]
​
需求: 根据key进行排序操作,演示降序
代码: rdd.sortByKey(ascending=False).collect()
结果: [(15, 3), (12, 4), (10, 2), (8, 4), (7, 4), (2, 4)]
​
​
输入: rdd = sc.parallelize([('a01',2),('A01',3),('a011',2),('a03',2),('a021',2),('a04',2)])
需求: 根据key进行排序操作,演示升序
代码: rdd.sortByKey().collect()
结果: [('A01', 3), ('a01', 2), ('a011', 2), ('a021', 2), ('a03', 2), ('a04', 2)]
总结: 对字符串的key排序的时候,按照ASCII码排序。大写字母排在小写字母前面;如果前面都一样,就继续比较后面的内容,没有内容的比有内容排在前面。

四.RDD的动作算子

  • collect() 算子:

    • 格式: collect()

    • 作用: 收集各个分区的数据,将数据汇总到一个大的列表返回

  • reduce() 算子:

    • 格式: reduce(fn)

    • 作用: 根据传入的函数对数据进行聚合操作

输入: rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10])
需求: 统计所有元素之和是多少
代码: rdd.reduce(lambda num1,num2:num1+num2)
结果: 55
  • first() 算子:

    • 格式: first()

    • 说明: 获取第一个元素

输入: rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10])
需求: 获取第一个元素
代码: rdd.first()
结果: 1
  • take() 算子

    • 格式: take(N)

    • 说明: 获取前N个元素 ,类似于 limit操作

输入: rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10])
需求: 获取前3个元素
代码: rdd.take(3)
结果: [1, 2, 3]
  • top()算子:

    • 格式: top(N,[fn])

    • 说明: 对数据集进行倒序排序操作,如果kv类型,针对key进行排序,获取前N个元素

    • fn: 可以自定义排序,按照谁来排序

输入: rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10])
需求: 获取前3个元素
代码: rdd.top(3)
结果: [10, 9, 8]
​
​
输入: rdd = sc.parallelize([('c01',5),('c02',8),('c04',1),('c03',4)])
需求: 按照班级人数排序,取前2个
代码: rdd.top(2,lambda tup:tup[1])
结果: [('c02', 8), ('c01', 5)]
  • count() 算子

输入: rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10])
需求: 获取一共有多少个元素
代码: rdd.count()
结果: 10
  • foreach() 算子

    • 格式: foreach(fn)

    • 作用: 对数据集进行遍历操作,遍历后做什么,取决于传入的fn

输入: rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10])
需求: 对数据进行遍历打印
代码: rdd.foreach(lambda num:print(num))
结果: 
6
7
8
9
10
1
2
3
4
5

五.RDD的重要算子

5.1 基本算子

5.2 分区算子

  • mapPartitions和foreachPartition

在spark中,对于map算子和foreach算子都提供了分区函数,分别是mapPartitions() 和 foreachPartition()。 map算子和foreach算子,针对的是RDD中每个分区下的每一条数据。而分区函数,则是一次性处理一个分区。RDD->多个分区,每个分区->多条数据

输入: 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]]

演示: map和mapPartitions
需求: 对数字加一
================================map==================================
自定义函数: 
def fn1(num):
    print(num)
    return num+1

rdd.map(fn1).collect()

结果: 
4
5
6
1
2
3
7
8
9
10
[2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

请问: fn1被调用了几次?
回答: 【10】
弊端: 【如果有消耗资源的操作,例如:数据库连接关闭、文件打开关闭,这样反复操作,运行效率低】


=============================mapPartitions===========================
自定义函数常规写法: 
def fn1(list):
    print(list)
    
    new_list = []
    for i in list:
        new_list.append(i + 1)
    return new_list

rdd.mapPartitions(fn1)

自定义函数简单写法: 
def fn1(list):
    print(list)
    
    for i in list:
        yield i + 1
    return list
    
rdd.mapPartitions(fn1)

结果: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
说明:yield作用将结果临时保存下来

演示: foreach和foreachPartition
需求: 遍历打印
==============================foreach================================
自定义函数: 
def fn1(num):
    print(num)

rdd.foreach(fn1)
结果:
4
5
6
1
2
3
7
8
9
10


==========================foreachPartition===========================
自定义函数: 
def fn1(list):
    print(list)
    
    for i in list:
        print(i)

rdd.foreachPartition(fn1)

结果:
<itertools.chain object at 0x7ffba3f94340>
4
5
6
<itertools.chain object at 0x7ffba3f94a60>
1
2
3
<itertools.chain object at 0x7ffba3f949a0>
7
8
9
10

总结: ​ 在使用map和foreach的时候,如果里面涉及到消耗资源(例如:数据库打开和关闭、文件打开和关闭)的操作,建议使用对应的分区算子mapPartitions和foreachPartition;如果没有,它们之间的效率基本相同,没有差别,选哪个都可以。

分区函数: 作用在每个分区上的 非分区函数: 作用在每条数据上的

5.3 重分区算子

repartition算子

  • 格式:repartition(分区数)

  • 作用:重新对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(15).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]]

  • coalesce算子

    • 格式:coalesce(分区数,[参数2]),参数2表示的是否开启shuffle,如果开启了,即可实现增大分区;如果不开启,仅能减少分区,默认是关闭shuffle

    • 作用:重新对RDD分区数量进行调整,得到一个新的RDD。默认只能减少分区数量

输入: 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(15).glom().collect()
结果: [[1, 2, 3], [4, 5, 6], [7, 8, 9, 10]]
发现增加分区没有生效
​
将参数2设置为True,再增大分区: rdd.coalesce(15,shuffle=True).glom().collect()
结果: [[], [1, 2, 3], [], [], [], [], [], [7, 8, 9, 10], [4, 5, 6], [], [], [], [], [], []]

repartition 和 coalesce总结:

1、repartition 和 coalesce两个算子都【可以修改RDD的分区数量】

2、repartition本质上是coalesce的一种当参数2为True的简写方案,因为repartition底层调用coalesce函数,将参数2设置为True

3、repartition既可以【增大分区】,也可以【减小分区】,触发shuffle

4、coalesce默认【只能减小分区】,无法增大分区,不触发shuffle。如果要增大分区,需要将参数2调整为True,这时就会触发shuffle

  • partitionBy算子

    • 格式:partitionBy(分区数,[参数2]) 参数2是自定义分区规则

    • 作用:专门针对kv类型数据进行重新分区,得到一个新的RDD。可以增大分区,也可以减少分区,但是会产生shuffle

    注意:

    默认根据key进行hash取模划分操作,如果不满意这个分区方案,可以通过参数2自定义分区规则。 自定义分区规则返回的必须是一个int类型的数据,它是分区的编号,编号从0开始

输入: 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)]
    ]
​
​
需求: 增大分区,尝试分为11个分区
代码: rdd.partitionBy(11).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,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)]
    ]

注意:

如果数据中,某个key的数据量占了很大的比例,会出现数据倾斜

5.4 聚合算子

  • 单值类型的聚合算子

    • reduce(fn1):根据传入函数对数据进行聚合处理

    • fold(defaultAgg,fn1):根据传入函数对数据进行聚合处理,同时支持给agg设置初始值

    • 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]]
需求: 求和计算, 求所有数据之和
​
================================reduce================================
代码: rdd.reduce(lambda num1,num2:num1+num2)
结果: 55
​
​
================================fold================================
代码: rdd.fold(5,lambda num1,num2:num1+num2)
结果: 75
​
​
================================aggregate================================
代码: 
def fn1(agg,curr):
    return agg+curr
    
def fn2(a,b):
    return a+b
​
rdd.aggregate(5,fn1,fn2)
结果: 75

总结:

reduce的底层是fold,fold底层是aggregate。如果需求能够使用reduce实现,就优先使用reduce;如果不能,再使用fold算子;如果fold算子也不行,就是使用aggregate

5.5关联算子

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

相关的算子:

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

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

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

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

输入:
rdd1 = sc.parallelize([('c01','张三'),('c02','李四'),('c02','王五'),('c01','赵六'),('c03','田七'),('c03','周八'),('c02','李九'),('c04','老张')])
    
rdd2 = sc.parallelize([('c01','大数据一班'),('c02','大数据二班'),('c03','大数据三班'),('c05','大数据五班')])


================================join================================
代码: rdd1.join(rdd2).collect()
结果: 
[
    ('c01', ('张三', '大数据一班')), 
    ('c01', ('赵六', '大数据一班')), 
    ('c02', ('李四', '大数据二班')), 
    ('c02', ('王五', '大数据二班')), 
    ('c02', ('李九', '大数据二班')), 
    ('c03', ('田七', '大数据三班')), 
    ('c03', ('周八', '大数据三班'))
]

       
================================leftOuterJoin================================
代码: rdd1.leftOuterJoin(rdd2).collect()
结果: 
[
    ('c04', ('老张', None)), 
    ('c01', ('张三', '大数据一班')), 
    ('c01', ('赵六', '大数据一班')), 
    ('c02', ('李四', '大数据二班')), 
    ('c02', ('王五', '大数据二班')), 
    ('c02', ('李九', '大数据二班')), 
    ('c03', ('田七', '大数据三班')), 
    ('c03', ('周八', '大数据三班'))
]

    

================================rightOuterJoin================================
代码: rdd1.rightOuterJoin(rdd2).collect()
结果: 
[
    ('c05', (None, '大数据五班')), 
    ('c01', ('张三', '大数据一班')), 
    ('c01', ('赵六', '大数据一班')),
    ('c02', ('李四', '大数据二班')), 
    ('c02', ('王五', '大数据二班')), 
    ('c02', ('李九', '大数据二班')), 
    ('c03', ('田七', '大数据三班')), 
    ('c03', ('周八', '大数据三班'))
]

        

================================fullOuterJoin================================
代码: rdd1.fullOuterJoin(rdd2).collect()
结果: 
[
    ('c04', ('老张', None)), 
    ('c05', (None, '大数据五班')), 
    ('c01', ('张三', '大数据一班')), 
    ('c01', ('赵六', '大数据一班')), 
    ('c02', ('李四', '大数据二班')), 
    ('c02', ('王五', '大数据二班')), 
    ('c02', ('李九', '大数据二班')), 
    ('c03', ('田七', '大数据三班')), 
    ('c03', ('周八', '大数据三班'))
]

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值