使用Spark数据集在Scala中执行类型化联接

使用Spark数据集在Scala中执行类型化联接,scala,apache-spark,join,apache-spark-sql,apache-spark-dataset,Scala,Apache Spark,Join,Apache Spark Sql,Apache Spark Dataset,我喜欢Spark数据集,因为它们在编译时会给我分析错误和语法错误,还允许我使用getter而不是硬编码的名称/数字。大多数计算可以通过Dataset的高级API完成。例如,与使用RDD行的数据字段相比,通过访问数据集类型的对象来执行agg、select、sum、avg、map、filter或groupBy操作要简单得多 然而,这里缺少连接操作,我读到我可以像这样进行连接 ds1.joinWith(ds2, ds1.toDF().col("key") === ds2.toDF().col("key

我喜欢Spark数据集,因为它们在编译时会给我分析错误和语法错误,还允许我使用getter而不是硬编码的名称/数字。大多数计算可以通过Dataset的高级API完成。例如,与使用RDD行的数据字段相比,通过访问数据集类型的对象来执行agg、select、sum、avg、map、filter或groupBy操作要简单得多

然而,这里缺少连接操作,我读到我可以像这样进行连接

ds1.joinWith(ds2, ds1.toDF().col("key") === ds2.toDF().col("key"), "inner")
ds1.joinWith(ds2, ds1.key === ds2.key, "inner")
但这不是我想要的,因为我更喜欢通过case类接口来实现,所以更像这样

ds1.joinWith(ds2, ds1.toDF().col("key") === ds2.toDF().col("key"), "inner")
ds1.joinWith(ds2, ds1.key === ds2.key, "inner")
现在最好的选择似乎是在case类旁边创建一个对象,并给这个函数提供一个字符串形式的正确列名。因此,我将使用第一行代码,但放置一个函数,而不是硬编码的列名。但这感觉不够优雅

有人能告诉我其他的选择吗?目标是从实际的列名中抽象出来,最好通过case类的getter来工作

我使用的是Spark 1.6.1和Scala 2.10 只有当连接条件基于相等运算符时,Spark SQL才能优化连接。这意味着我们可以分别考虑等值连接和非等值连接。 等分 Equijoin可以通过将
数据集
映射到(键,值)元组,基于键执行连接,并重塑结果,以类型安全的方式实现:

import org.apache.spark.sql.Encoder
import org.apache.spark.sql.Dataset

def safeEquiJoin[T, U, K](ds1: Dataset[T], ds2: Dataset[U])
    (f: T => K, g: U => K)
    (implicit e1: Encoder[(K, T)], e2: Encoder[(K, U)], e3: Encoder[(T, U)]) = {
  val ds1_ = ds1.map(x => (f(x), x))
  val ds2_ = ds2.map(x => (g(x), x))
  ds1_.joinWith(ds2_, ds1_("_1") === ds2_("_1")).map(x => (x._1._2, x._2._2))
}
非等联接 可以使用关系代数运算符表示为R⋈θS=σθ(R×S),并直接转换为代码

火花2.0 启用
crossJoin
并将
joinWith
与微不足道的相等谓词一起使用:

spark.conf.set("spark.sql.crossJoin.enabled", true)

def safeNonEquiJoin[T, U](ds1: Dataset[T], ds2: Dataset[U])
                         (p: (T, U) => Boolean) = {
  ds1.joinWith(ds2, lit(true)).filter(p.tupled)
}
火花2.1 使用
crossJoin
方法:

def safeNonEquiJoin[T, U](ds1: Dataset[T], ds2: Dataset[U])
    (p: (T, U) => Boolean)
    (implicit e1: Encoder[Tuple1[T]], e2: Encoder[Tuple1[U]], e3: Encoder[(T, U)]) = {
  ds1.map(Tuple1(_)).crossJoin(ds2.map(Tuple1(_))).as[(T, U)].filter(p.tupled)
}
例子 笔记
  • 应该注意的是,这些方法与直接的
    joinWith
    应用程序有很大不同,需要昂贵的
    反序列化对象
    /
    序列化对象
    转换(与直接的
    joinWith
    可以对数据使用逻辑操作相比)

    这与中描述的行为类似

  • 如果您不局限于Spark SQL API,它为
    数据集提供了有趣的类型安全扩展(到今天为止,它只支持Spark 2.0):

  • Dataset
    API在1.6中不稳定,所以我认为在那里使用它没有意义

  • 当然,这种设计和描述性名称是不必要的。您可以很容易地使用type类将此方法隐式添加到
    数据集
    中,因为它与内置签名没有冲突,所以两者都可以称为
    joinWith


此外,非类型安全Spark API的另一个更大问题是,当您加入两个
数据集时,它将为您提供一个
数据帧。然后,您将丢失原始两个数据集中的类型

val a: Dataset[A]
val b: Dataset[B]

val joined: Dataframe = a.join(b)
// what would be great is 
val joined: Dataset[C] = a.join(b)(implicit func: (A, B) => C)

safeEquiJoin
示例只是通过将
joinWith
的调用封装在引号中指定元组成员(
“\u 1”
)来强化没有现成的方式来进行完全类型的安全连接在一个很好的包装器中,它的实现往往是粗糙的。@n尽管我同意总体观点,但您必须记住,
Dataset
API根本不是类型安全的。它只是对有效的非类型化容器和本机内存访问的抽象。在您将
调用为[T]
时,它与依赖
asInsnanceOf
和按名称匹配字段一样好。如果您正在寻找端到端类型的安全性,那么
RDD
API仍然是不可替代的。“类型安全”连接有更多优雅的实现,但尽管令人不满意,但最终他们还是会做同样的事情(匹配名称),并希望达到最佳效果。代码片段所演示或指向的内容并不十分清楚actually@matanster又称作然后可以在编译时测试联接的结果类型。e、 g.如果结果为out:Dataset[C],当类型C没有fieldA时调用.map(u.fieldA=>…)将在编译过程中失败,不需要在运行时分解。@matanster它基本上意味着
join
Dataset
中的非类型化转换。