目录
11.连接
11.1 无类型连接算子 join 的 API
11.2 连接类型
11.2.1 交叉连接 - cross交叉
11.2.2 内连接 - inner
11.2.3 全外连接
11.2.4 左外连接
11.2.5 LeftAnti - 只包含左边集合中没连接上的数据
11.2.6 LeftSemi - 只包含左侧集合中连接上的数据
11.2.7 右外连接
11.3 广播连接【扩展】
导读
无类型连接 join
连接类型 Join Types
11.1
无类型连接算子 join
的 API
Step 1: 什么是连接
按照 PostgreSQL 的文档中所说, 只要能在一个查询中, 同一时间并发的访问多条数据, 就叫做连接.
做到这件事有两种方式:
一种是把两张表在逻辑上连接起来, 一条语句中同时访问两张表
select * from user join address on user.address_id = address.id
还有一种方式就是表连接自己, 一条语句也能访问自己中的多条数据
select * from user u1 join (select * from user) u2 on u1.id = u2.id
Step 2: join
算子的使用非常简单, 大致的调用方式如下
join(right: Dataset[_], joinExprs: Column, joinType: String): DataFrame
Step 3: 简单连接案例
表结构如下
如果希望对这两张表进行连接, 首先应该注意的是可以连接的字段, 比如说此处的左侧表 cityId
和右侧表 id
就是可以连接的字段, 使用 join
算子就可以将两个表连接起来, 进行统一的查询
val person = Seq((0, "Lucy", 0), (1, "Lily", 0), (2, "Tim", 2), (3, "Danial", 0))
.toDF("id", "name", "cityId")
val cities = Seq((0, "Beijing"), (1, "Shanghai"), (2, "Guangzhou"))
.toDF("id", "name")
person.join(cities, person.col("cityId") === cities.col("id"))
.select(person.col("id"),
person.col("name"),
cities.col("name") as "city")
.show()
/**
* 执行结果:
*
* +---+------+---------+
* | id| name| city|
* +---+------+---------+
* | 0| Lucy| Beijing|
* | 1| Lily| Beijing|
* | 2| Tim|Guangzhou|
* | 3|Danial| Beijing|
* +---+------+---------+
*/
Step 4: 什么是连接?
现在两个表连接得到了如下的表
通过对这张表的查询, 这个查询是作用于两张表的, 所以是同一时间访问了多条数据
spark.sql("select name from user_city where city = 'Beijing'").show()
/**
* 执行结果
*
* +------+
* | name|
* +------+
* | Lucy|
* | Lily|
* |Danial|
* +------+
*/
数据准备:
val spark = SparkSession.builder().master("local[6]").appName("join").getOrCreate()
import spark.implicits._
val person = Seq((0, "Lucy", 0), (1, "Lily", 0), (2, "Tim", 2), (3, "Danial", 3))
.toDF("id", "name", "cityId")
person.createOrReplaceTempView("person")
val cities = Seq((0, "Beijing"), (1, "Shanghai"), (2, "Guangzhou"))
.toDF("id", "name")
cities.createOrReplaceTempView("cities")
连接就是笛卡尔积, 就是两个表中所有的数据两两结对
交叉连接是一个非常重的操作, 在生产中, 尽量不要将两个大数据集交叉连接, 如果一定要交叉连接, 也需要在交叉连接后进行过滤, 优化器会进行优化
/**
* 交叉连接 crossJoin
*/
@Test
def crossJoin():Unit = {
//命令式API
person.crossJoin(cities)
.where(person.col("cityId") === cities.col("id"))
.show()
//SQL语句
spark.sql("select u.id,u.name,c.name from person u cross join cities c " +
"where u.cityId = c.id")
.show()
}
内连接就是按照条件找到两个数据集关联的数据, 并且在生成的结果集中只存在能关联到的数据
@Test
def introJoin() : Unit = {
//命令式API
val df = person.join(cities,person.col("cityId") === cities.col("id"))
.select(
person.col("id"),
person.col("name"),
cities.col("name") as "city")
.show()
//SQL语句
df.createOrReplaceTempView("user_city")
spark.sql("select id,name,city from user_city where city = 'Beijing'").show()
}
全外连接分为:outer
, full
, fullouter
内连接和外连接的最大区别, 就是内连接的结果集中只有可以连接上的数据, 而外连接可以包含没有连接上的数据, 根据情况的不同, 外连接又可以分为很多种, 比如所有的没连接上的数据都放入结果集, 就叫做全外连接
/**
* 全外连接 左、右全部包含
*/
@Test
def fullOuter(): Unit = {
//内连接:只显示能连接上的数据 外连接:包含一部分没有连接上的数据 全外链接:指左右两边没有连接上的数据都显示出来
person.join(cities,
person.col("cityId") === cities.col("id"),
joinType = "full")
.show()
spark.sql("select p.id,p.name,c.name " +
"from person p full outer join cities c " +
"on p.cityId = c.id").show()
}
左外连接分为:leftouter
, left
左外连接是全外连接的一个子集, 全外连接中包含左右两边数据集没有连接上的数据, 而左外连接只包含左边数据集中没有连接上的数据
/**
* 左外连接、右外连接
*/
@Test
def leftRight():Unit = {
//左连接
person.join(cities,
person.col("cityId") === cities.col("id"),
joinType = "left")
.show()
spark.sql("select p.id,p.name,c.name " +
"from person p left join cities c " +
"on p.cityId = c.id").show()
//右连接 与上面一致 left -> right
}
LeftAnti
是一种特殊的连接形式, 和左外连接类似, 但是其结果集中没有右侧的数据, 只包含左边集合中没连接上的数据
//leftAnti: 只包含左侧没有连接上的数据
person.join(cities,
person.col("cityId") === cities.col("id"),
joinType = "leftanti")
.show()
spark.sql("select p.id,p.name " +
"from person p left anti join cities c " +
"on p.cityId = c.id").show()
和 LeftAnti
恰好相反, LeftSemi
的结果集也没有右侧集合的数据, 但是只包含左侧集合中连接上的数据
//leftSemi:只包含左侧连接上的数据
person.join(cities,
person.col("cityId") === cities.col("id"),
joinType = "leftsemi")
.show()
spark.sql("select p.id,p.name " +
"from person p left semi join cities c " +
"on p.cityId = c.id").show()
右外连接分为:rightouter
, right
操作与左外连接一致
右外连接和左外连接刚好相反, 左外是包含左侧未连接的数据, 和两个数据集中连接上的数据, 而右外是包含右侧未连接的数据, 和两个数据集中连接上的数据
select * from person right join cities on person.cityId = cities.id
person.join(right = cities,
joinExprs = person("cityId") === cities("id"),
joinType = "right") // rightouter, right
.show()
Step 1: 正常情况下的 Join
过程
Join
会在集群中分发两个数据集, 两个数据集都要复制到 Reducer
端, 是一个非常复杂和标准的 ShuffleDependency
, 有什么可以优化效率吗?
Step 2: Map
端 Join
前面图中看的过程, 之所以说它效率很低, 原因是需要在集群中进行数据拷贝, 如果能减少数据拷贝, 就能减少开销
如果能够只分发一个较小的数据集呢?
可以将小数据集收集起来, 分发给每一个 Executor
, 然后在需要 Join
的时候, 让较大的数据集在 Map
端直接获取小数据集, 从而进行 Join
, 这种方式是不需要进行 Shuffle
的, 所以称之为 Map
端 Join
Step 3: Map
端 Join
的常规实现
如果使用 RDD
的话, 该如何实现 Map
端 Join
呢?
val personRDD = spark.sparkContext.parallelize(Seq((0, "Lucy", 0),
(1, "Lily", 0), (2, "Tim", 2), (3, "Danial", 3)))
val citiesRDD = spark.sparkContext.parallelize(Seq((0, "Beijing"),
(1, "Shanghai"), (2, "Guangzhou")))
val citiesBroadcast = spark.sparkContext.broadcast(citiesRDD.collectAsMap())
val result = personRDD.mapPartitions(
iter => {
val citiesMap = citiesBroadcast.value
// 使用列表生成式 yield 生成列表
val result = for (person <- iter if citiesMap.contains(person._3))
yield (person._1, person._2, citiesMap(person._3))
result
}
).collect()
result.foreach(println(_))
Step 4: 使用 Dataset
实现 Join
的时候会自动进行 Map
端 Join
自动进行 Map
端 Join
需要依赖一个系统参数 spark.sql.autoBroadcastJoinThreshold
, 当数据集小于这个参数的大小时, 会自动进行 Map
端 Join
如下, 开启自动 Join
println(spark.conf.get("spark.sql.autoBroadcastJoinThreshold").toInt / 1024 / 1024)
println(person.crossJoin(cities).queryExecution.sparkPlan.numberedTreeString)
当关闭这个参数的时候, 则不会自动 Map 端 Join 了
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", -1)
println(person.crossJoin(cities).queryExecution.sparkPlan.numberedTreeString)
Step 5: 也可以使用函数强制开启 Map 端 Join
在使用 Dataset 的 join 时, 可以使用 broadcast 函数来实现 Map 端 Join
import org.apache.spark.sql.functions._
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", -1)
println(person.crossJoin(broadcast(cities)).queryExecution.sparkPlan.numberedTreeString)
即使是使用 SQL 也可以使用特殊的语法开启
val resultDF = spark.sql(
"""
|select /*+ MAPJOIN (rt) */ * from person cross join cities rt
""".stripMargin)
println(resultDF.queryExecution.sparkPlan.numberedTreeString)