Spark(三)-- SparkSQL扩展(数据操作) -- 连接(五)

目录

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 广播连接【扩展】


11.连接

导读

  1. 无类型连接 join

  2. 连接类型 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|
  * +------+
  */

11.2 连接类型

数据准备:

  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")

11.2.1 交叉连接 - cross交叉

连接就是笛卡尔积, 就是两个表中所有的数据两两结对

交叉连接是一个非常重的操作, 在生产中, 尽量不要将两个大数据集交叉连接, 如果一定要交叉连接, 也需要在交叉连接后进行过滤, 优化器会进行优化

  /**
    * 交叉连接 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()
  }

11.2.2 内连接 - inner

内连接就是按照条件找到两个数据集关联的数据, 并且在生成的结果集中只存在能关联到的数据

  @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()
  }

11.2.3 全外连接

全外连接分为:outerfullfullouter

内连接和外连接的最大区别, 就是内连接的结果集中只有可以连接上的数据, 而外连接可以包含没有连接上的数据, 根据情况的不同, 外连接又可以分为很多种, 比如所有的没连接上的数据都放入结果集, 就叫做全外连接

  /**
    * 全外连接 左、右全部包含
    */
  @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()

  }

11.2.4 左外连接

左外连接分为:leftouterleft

左外连接是全外连接的一个子集, 全外连接中包含左右两边数据集没有连接上的数据, 而左外连接只包含左边数据集中没有连接上的数据

  /**
    * 左外连接、右外连接
    */
  @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
  }

11.2.5 LeftAnti - 只包含左边集合中没连接上的数据

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()

11.2.6 LeftSemi - 只包含左侧集合中连接上的数据

和 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()

11.2.7 右外连接

右外连接分为:rightouterright 操作与左外连接一致

右外连接和左外连接刚好相反, 左外是包含左侧未连接的数据, 和两个数据集中连接上的数据, 而右外是包含右侧未连接的数据, 和两个数据集中连接上的数据

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()

11.3 广播连接【扩展】

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)

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值