Spark pairRDD(键值对)操作:聚合、分组、连接、排序

Spark 为包含键值对类型的RDD 提供了一些专有的操作。这些RDD 被称为pair RDD。Pair RDD 是很多程序的构成要素,因为它们提供了并行操作各个键或跨节点重新进行数据分组的操作接口。我们通常从一个RDD 中提取某些字段(例如代表事件时间、用户ID 或者其他标识符的字段),并使用这些字段作为pair RDD 操作中的键。

1 Pair RDD的转化操作

Pair RDD 可以使用所有标准RDD 上的可用的转化操作。由于pair RDD 中包含二元组,所以需要传递的函数应当操作二元组而不是独立的元素。
在这里插入图片描述

filter

Pair RDD 也还是RDD(元素为Java 或Scala 中的Tuple2 对象或Python 中的元组),因此同样支持RDD 所支持的函数。例如,我们可以对pair RDD筛选掉长度超过20个字符的行:

pairs.filter{case (key, value) => value.length < 20} // 用Scala 对第二个元素进行筛选

在这里插入图片描述mapValues

有时,我们只想访问pair RDD的值部分,这时操作二元组很麻烦。由于这是一种常见的使用模式,因此Spark 提供了mapValues(func) 函数,功能类似于map{case (x, y): (x,func(y))}。可以在很多例子中使用这个函数。接下来就依次讨论pair RDD 的各种操作,先从聚合操作开始。

1.1 聚合操作

当数据集以键值对形式组织的时候,聚合具有相同键的元素进行一些统计是很常见的操作。之前讲解过基础RDD上的fold()、combine()、reduce() 等行动操作,pair RDD 上则有相应的针对键的转化操作。Spark 有一组类似的操作,可以组合具有相同键的值。这些操作返回RDD,因此它们是转化操作而不是行动操作。

reduceByKey

reduceByKey() 与reduce() 相当类似;它们都接收一个函数,并使用该函数对值进行合并。reduceByKey() 会为数据集中的每个键进行并行的归约操作,每个归约操作会将键相同的值合并起来。因为数据集中可能有大量的键,所以reduceByKey() 没有被实现为向用户程序返回一个值的行动操作。实际上,它会返回一个由各键和对应键归约出来的结果值组成的新的RDD。

rdd.mapValues(x => (x, 1)).reduceByKey((x, y) => (x._1 + y._1, x._2 + y._2)) // 在Scala 中使用reduceByKey() 和mapValues() 计算每个键对应的平均值

在这里插入图片描述
foldByKey

foldByKey() 则与fold() 相当类似;它们都使用一个与RDD 和合并函数中的数据类型相
同的零值作为初始值。与fold() 一样,foldByKey() 操作所使用的合并函数对零值与另一个元素进行合并,结果仍为该元素。

对于解决经典的分布式单词计数问题,可以使用flatMap() 来生成以单词为键、以数字1 为值的pair RDD,然后使用reduceByKey() 对所有的单词进行计数。

val input = sc.textFile("s3://...")
val words = input.flatMap(x => x.split(" "))
val result = words.map(x => (x, 1)).reduceByKey((x, y) => x + y)

事实上,我们可以对第一个RDD 使用countByValue() 函数,以更快地实现
单词计数:input.flatMap(x => x.split(" ")).countByValue()。countByValue统计一个RDD中各个value(这里即各个单词)的出现次数。返回一个map,map的key是元素的值,value是出现的次数。

combineByKey

combineByKey() 是最为常用的基于键进行聚合的函数。大多数基于键聚合的函数都是用它实现的。和aggregate() 一样,combineByKey() 可以让用户返回与输入数据的类型不同的返回值。

要理解combineByKey(), 要先理解它在处理数据时是如何处理每个元素的。由于
combineByKey() 会遍历分区中的所有元素,因此每个元素的键要么还没有遇到过,要么就和之前的某个元素的键相同。

如果这是一个新的元素,combineByKey() 会使用一个叫作createCombiner() 的函数来创建那个键对应的累加器的初始值(该键对应的value,初始计数1)。需要注意的是,这一过程会在每个分区中第一次出现各个键时发生,而不是在整个RDD 中第一次出现一个键时发生。

如果这是一个在处理当前分区之前已经遇到的键,它会使用mergeValue() 方法将该键的累加器对应的当前值与这个新的值进行合并(新遇到的已存在key对应的value + 累加器的value, 累加器的计数+1)。

由于每个分区都是独立处理的,因此对于同一个键可以有多个累加器。如果有两个或者更多的分区都有对应同一个键的累加器,就需要使用用户提供的mergeCombiners() 方法将各个分区的结果进行合并。

combineByKey() 有多个参数分别对应聚合操作的各个阶段,因而非常适合用来解释聚合操作各个阶段的功能划分。为了更好地演示combineByKey() 是如何工作的,下面来看看如何计算各键对应的平均值。

// 在Scala 中使用combineByKey() 求每个键对应的平均值
val result = input.combineByKey(
(v) => (v, 1), // 分区内遇到一个新的key,将其对应的value(v)和1作为累加器的初始值
(acc: (Int, Int), v) => (acc._1 + v, acc._2 + 1),// 分区内遇到一个之前已遇到的key时,将其对应的value(v)合并到该key对应的累加器acc中,acc._1 + v表示合并value,acc._2 + 1表示累加个数
(acc1: (Int, Int), acc2: (Int, Int)) => (acc1._1 + acc2._1, acc1._2 + acc2._2) //不同分区中存在同一个key对应的多个累加器acc,合并acc
).map{ case (key, acc) => (key, acc._1 / acc._2.toFloat) // combineByKey返回(key,(key_sumvalues, key_counts)),对每个元组求key对应的平均值}
result.collectAsMap().map(println(_))

在这里插入图片描述
每个RDD都有固定数目的分区,分区数决定了在RDD上执行操作时的并行度。在执行聚合或分组操作时,可以要求Spark 使用给定的分区数。Spark 始终尝试根据集群
的大小推断出一个有意义的默认值,但是有时候你可能要对并行度进行调优来获取更好的性能表现。

// 在Scala 中自定义reduceByKey()的并行度
val data = Seq(("a", 3), ("b", 4), ("a", 1))
sc.parallelize(data).reduceByKey((x, y) => x + y) // 默认并行度
sc.parallelize(data).reduceByKey((x, y) => x + y, 10) // 自定义并行度

有时,我们希望在除分组操作和聚合操作之外的操作中也能改变RDD的分区。对于
这样的情况,Spark 提供了repartition() 函数。它会把数据通过网络进行混洗,并创
建出新的分区集合。切记,对数据进行重新分区是代价相对比较大的操作。Spark中也有一个优化版的repartition(),叫作coalesce()。你可以使用Java或Scala中的rdd.
partitions.size()以及Python中的rdd.getNumPartitions查看RDD的分区数,并确保调
用coalesce()时将RDD 合并到比现在的分区数更少的分区中。

1.2 数据分组

groupByKey

对于有键的数据,一个常见的用例是将数据根据键进行分组——比如查看一个顾客的所有订单。如果数据已经以预期的方式提取了键,groupByKey() 就会使用RDD中的键来对数据进行分组。对于一个由类型K的键和类型V的值组成的RDD,所得到的结果RDD类型会是[K, Iterable[V]]。

groupBy

groupBy()可以用于未成对的数据上,也可以根据除键相同以外的条件进行分组。它可以接收一个函数,对源RDD中的每个元素使用该函数,将返回结果作为键再进行分组。

val a = sc.parallelize(1 to 9, 3) 
a.groupBy(x => { if (x % 2 == 0) "even" else "odd" }).collect//分成两组 
/*结果 
Array(
(even,ArrayBuffer(2, 4, 6, 8)),
(odd,ArrayBuffer(1, 3, 5, 7, 9))
)
*/
val a = sc.parallelize(1 to 9, 3)
def myfunc(a: Int) : Int =
{
  a % 2//分成两组
}
a.groupBy(myfunc).collect
/*结果
Array(
(0,ArrayBuffer(2, 4, 6, 8)),
(1,ArrayBuffer(1, 3, 5, 7, 9))
)
*/

cogroup

除了对单个RDD的数据进行分组,还可以使用一个叫作cogroup()的函数对多个共享同
一个键的RDD进行分组。对两个键的类型均为K 而值的类型分别为V和W的RDD进行
cogroup()时,得到的结果RDD类型为[(K, (Iterable[V], Iterable[W]))]。如果其中的一个RDD对于另一个RDD中存在的某个键没有对应的记录,那么对应的迭代器则为空。

val DBName=Array(
  Tuple2(1,"Spark"),
  Tuple2(2,"Hadoop"),
  Tuple2(3,"Kylin"),
  Tuple2(4,"Flink")
)
val numType=Array(
  Tuple2(1,"String"),
  Tuple2(2,"int"),
  Tuple2(3,"byte"),
  Tuple2(4,"bollean"),
  Tuple2(5,"float"),
  Tuple2(1,"34"),
  Tuple2(1,"45"),
  Tuple2(2,"47"),
  Tuple2(3,"75"),
  Tuple2(4,"95"),
  Tuple2(5,"16"),
  Tuple2(1,"85")
)
val names=sc.parallelize(DBName)
val types=sc.parallelize(numType)
val nameAndType=names.cogroup(types)  
nameAndType.collect.foreach(println)
/*结果
(4,(CompactBuffer(Flink),CompactBuffer(bollean, 95)))
(1,(CompactBuffer(Spark),CompactBuffer(String, 34, 45, 85)))
(3,(CompactBuffer(Kylin),CompactBuffer(byte, 75)))
(5,(CompactBuffer(),CompactBuffer(float, 16)))
(2,(CompactBuffer(Hadoop),CompactBuffer(int, 47)))
*/
1.3 连接

将有键的数据与另一组有键的数据一起使用是对键值对数据执行的最有用的操作之一。连接数据可能是pair RDD 最常用的操作之一。连接方式多种多样:右外连接、左外连接、交叉连接以及内连接。
在这里插入图片描述在这里插入图片描述
join

普通的join 操作符表示内连接2。只有在两个pair RDD 中都存在的键才叫输出。当一个输入对应的某个键有多个值时,生成的pair RDD 会包括来自两个输入RDD 的每一组相对应的记录。

storeAddress = {
(Store("Ritual"), "1026 Valencia St"), (Store("Philz"), "748 Van Ness Ave"),
(Store("Philz"), "3101 24th St"), (Store("Starbucks"), "Seattle")}

storeRating = {
(Store("Ritual"), 4.9), (Store("Philz"), 4.8))}

storeAddress.join(storeRating) == {
(Store("Ritual"), ("1026 Valencia St", 4.9)),
(Store("Philz"), ("748 Van Ness Ave", 4.8)),
(Store("Philz"), ("3101 24th St", 4.8))}

leftOuterJoin / rightOuterJoin

有时,我们不希望结果中的键必须在两个RDD 中都存在。例如,在连接客户信息与推荐时,如果一些客户还没有收到推荐,我们仍然不希望丢掉这些顾客。

leftOuterJoin(other)和rightOuterJoin(other) 都会根据键连接两个RDD,但是允许结果中存在其中的一个pair RDD 所缺失的键。

在使用leftOuterJoin() 产生的pair RDD 中,源RDD 的每一个键都有对应的记录。每个
键相应的值是由一个源RDD 中的值与一个包含第二个RDD 的值的Option(在Java 中为
Optional)对象组成的二元组。在Python 中,如果一个值不存在,则使用None 来表示;而数据存在时就用常规的值来表示,不使用任何封装。和join() 一样,每个键可以得到多条记录;当这种情况发生时,我们会得到两个RDD 中对应同一个键的两组值的笛卡尔积。

rightOuterJoin() 几乎与leftOuterJoin() 完全一样,只不过预期结果中的键必须出现在
第二个RDD 中,而二元组中的可缺失的部分则来自于源RDD 而非第二个RDD。

storeAddress = {
(Store("Ritual"), "1026 Valencia St"), (Store("Philz"), "748 Van Ness Ave"),
(Store("Philz"), "3101 24th St"), (Store("Starbucks"), "Seattle")}

storeRating = {
(Store("Ritual"), 4.9), (Store("Philz"), 4.8))}

storeAddress.leftOuterJoin(storeRating) ==
{(Store("Ritual"),("1026 Valencia St",Some(4.9))),
(Store("Starbucks"),("Seattle",None)),
(Store("Philz"),("748 Van Ness Ave",Some(4.8))),
(Store("Philz"),("3101 24th St",Some(4.8)))}

storeAddress.rightOuterJoin(storeRating) ==
{(Store("Ritual"),(Some("1026 Valencia St"),4.9)),
(Store("Philz"),(Some("748 Van Ness Ave"),4.8)),
(Store("Philz"), (Some("3101 24th St"),4.8))}
1.4 数据排序

sortByKey

我们经常要将RDD 倒序排列,因此sortByKey() 函数接收一个叫作ascending 的参数,表示我们是否想要让结果按升序排序(默认值为true)。有时我们也可能想按完全不同的排序依据进行排序。要支持这种情况,我们可以提供自定义的比较函数。下面会将整数转为字符串,然后使用字符串比较函数来对RDD 进行排序。

scala> val b = sc.parallelize(List(3,1,9,12,4))
b: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[38] at parallelize at <console>:12
 
scala> val c = b.zip(a)
c: org.apache.spark.rdd.RDD[(Int, String)] = ZippedPartitionsRDD2[39] at zip at <console>:16
 
scala> c.sortByKey().collect
res15: Array[(Int, String)] = Array((1,iteblog), (3,wyp), (4,test), (9,com), (12,397090770))
 
scala> implicit val sortIntegersByString = new Ordering[Int]{
     | override def compare(a: Int, b: Int) =
     | a.toString.compare(b.toString)}
sortIntegersByString: Ordering[Int] = $iwC$$iwC$$iwC$$iwC$$iwC$$anon$1@5d533f7a
 
scala>  c.sortByKey().collect
res17: Array[(Int, String)] = Array((1,iteblog), (12,397090770), (3,wyp), (4,test), (9,com))
 
scala> c.sortByKey().collect
res11: Array[(String, Int)] = Array((397090770,4), (com,3), (iteblog,2), (test,5), (wyp,1))

例子中的sortIntegersByString就是修改了默认的排序规则。这样将默认按照Int大小排序改成了对字符串的排序,所以12会排序在3之前。

1.4 Pair RDD的行动操作

和转化操作一样,所有基础RDD 支持的传统行动操作也都在pair RDD 上可用。Pair RDD提供了一些额外的行动操作,可以让我们充分利用数据的键值对特性。
在这里插入图片描述
参考 《Spark快速大数据分析》

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值