一起学习Spark(四)Spark SQL、DataFrames和Datasets

本文深入探讨SparkSQL和Dataset的概念,介绍如何使用SparkSession创建DataFrame和Dataset,涵盖数据读取、转换、聚合及自定义函数的实现。同时,对比RDD与Dataset的特性,指导开发者在大数据处理中做出合适的选择。

概念简介

Spark SQL是Spark用来处理结构化数据的模块,与基本的Spark RDD API不同,Spark SQL提供的接口为Spark提供了关于数据和计算执行的结构的更多信息。在内部,Spark SQL使用这些额外的信息来执行额外的优化。有几种与Spark SQL交互的方法,包括SQL和DataSet API,他们在计算结果时使用相同的执行引擎,而不是依赖于用来表达计算的API或编程语言。这种统一意味着开发人员可以很容易地在不同的api之间来回切换,这些api提供了最自然的方式来表达给定的转换。

Spark SQL

Spark SQL是Spark用来处理结构化数据的一个模块(结构化数据可以来自外部结构化数据源也可以通过RDD获取),使用Spark SQL的一种方式就是通过SQL语句来执行SQL查询。当在编程语言中使用SQL查询后,其返回结果会被封装为一个DataFrame/DataSet。

Datasets and DataFrames

DataFrame的前身是SchemaRDD(RDD+Schema=DataFrame),后SchemaRDD更名为DataFrame。与SchemaRDD的主要区别是:DataFrame不再直接继承自RDD,而是自己实现了RDD的绝大多数功能。你仍旧可以在DataFrame上调用.rdd方法将其转换为一个RDD。RDD可看作是分布式的对象的集合,Spark并不知道对象的详细模式信息,DataFrame可看作是分布式的Row对象的集合,其提供了由列组成的详细模式信息,使得Spark SQL可以进行某些形式的执行优化。DataFrame和普通的RDD的逻辑框架区别如下所示:

数据

 

DataSet是数据的分布式集合,是Spark 1.6新添加的接口,其目的是想结合RDD的好处(强类型(这意味着可以在编译时进行类型安全检查)、可以使用强大的lambda函数)和Spark SQL的优化执行引擎的好处。可以从JVM对象构造出Dataset,然后使用类似于RDD的函数式转换算子(map/flatMap/filter等)对其进行操作。

Dataset通过Encoder实现了自定义的序列化格式,使得某些操作可以在无需解序列化的情况下直接进行。另外Dataset还进行了包括Tungsten优化在内的很多性能方面的优化。

实际上Dataset是包含了DataFrame的功能的,这样二者就出现了很大的冗余,故Spark 2.0将二者统一:保留Dataset API,把DataFrame表示为Dataset[Row],即Dataset的子集。

Spark在迅速的发展,从原始的RDD API,再到DataFrame API,再到Dataset的出现,速度可谓惊人,执行性能上也有了很大提升。

我们在使用API时,应该优先选择DataFrame & Dataset,因为它的性能很好,而且以后的优化它都可以享受到,但是为了兼容早期版本的程序,RDD API也会一直保留着。后续Spark上层的库将全部会用 DataFrame & Dataset,比如MLlib、Streaming、Graphx等。

 入门指南

起点:SparkSession

Spark中所有功能的入口点都是SparkSession类。要创建一个基本的SparkSession,只需使用SparkSession.builder():

        import org.apache.spark.sql.SparkSession

        val spark = SparkSession
          .builder()
          .appName("Spark SQL basic example")
          .config("spark.some.config.option", "some-value")
          .getOrCreate()

        //隐式转换引入某些操作,例如将RDDs转换为DataFrames
        import spark.implicits._

Spark 2.0中的SparkSession为Hive特性提供了内置支持,包括使用HiveQL编写查询语句,访问HDFS UDF以及从Hive表读取数据的能力。要使用这些特性,甚至不需要安装Hive。

创建DataFrames

有了SparkSession后,应用程序可以从现有的RDD、Hive表或Spark数据源来创建DataFrames。

例如,下面根据JSON文件的内容创建一个DataFrame:

val df = spark.read.json("examples/src/main/resources/people.json")

// Displays the content of the DataFrame to stdout
df.show()
// +----+-------+
// | age|   name|
// +----+-------+
// |null|Michael|
// |  30|   Andy|
// |  19| Justin|
// +----+-------+

非强类型DataSet(也就是DataFrame)操作实例

DataFrame提供的是对结构化数据的处理,在Spark2.0中,DataFrame=DataSet[Row],这些操作也被称为“非强类型转换”,与DataSet的强类型转换相对。

下面是一些使用的实例:

                import spark.implicits._
                //以树形格式打印出schema
                df.printSchema()
                // root
                // |-- age: long (nullable = true)
                // |-- name: string (nullable = true)
                //查询name列的内容
                df.select("name").show()
                // +-------+
                // |   name|
                // +-------+
                // |Michael|
                // |   Andy|
                // | Justin|
                // +-------+

有关完整的操作列表,请参阅api文档

使用sql查询

SparkSession上的sql函数允许应用程序以编程方式运行sql查询,并以DataFrame的形式返回结果。

//将DataFrame注册为SQL临时视图
df.createOrReplaceTempView("people")

val sqlDF = spark.sql("SELECT * FROM people")
sqlDF.show()
// +----+-------+
// | age|   name|
// +----+-------+
// |null|Michael|
// |  30|   Andy|
// |  19| Justin|
// +----+-------+

全局临时视图

Spark SQL中的临时视图是会话范围的,如果创建它的会话终止了,它将会消失。如果您希望在所有会话之间共享一个临时视图,并保持活动状态,直到Spark应用程序终止,你可以创建一个全局临时视图,全局临时视图绑定到系统的数据库global_temp,必须使用限定名来引用它,例如:select * from global_view.view1.

// 将DataFrame注册为全局临时视图
df.createGlobalTempView("people")

// 全局临时视图绑定到系统保留的数据库' global_temp '
spark.sql("SELECT * FROM global_temp.people").show()
// +----+-------+
// | age|   name|
// +----+-------+
// |null|Michael|
// |  30|   Andy|
// |  19| Justin|
// +----+-------+

// 全局临时视图是跨会话的
spark.newSession().sql("SELECT * FROM global_temp.people").show()
// +----+-------+
// | age|   name|
// +----+-------+
// |null|Michael|
// |  30|   Andy|
// |  19| Justin|
// +----+-------+

创建DataSets

DataSet类似于RDD,但是,它们不使用Java序列化或Kryo,而是使用专门的编码器序列化对象,以便通过网络进行处理或传输。虽然encoder和标准序列化都负责将对象转换成字节,但encoder是动态生成的代码,使用的格式允许Spark执行许多操作,如过滤、排序和哈希,而无需将字节反序列化回对象。

case class Person(name: String, age: Long)

// case class的编码器会被创建
val caseClassDS = Seq(Person("Andy", 32)).toDS()
caseClassDS.show()
// +----+---+
// |name|age|
// +----+---+
// |Andy| 32|
// +----+---+

//大多数常见类型的编码器都是通过导入spark.implicits._自动提供的
val primitiveDS = Seq(1, 2, 3).toDS()
primitiveDS.map(_ + 1).collect() // 返回 Array(2, 3, 4)

// DataFrames可以通过提供一个类被转换成Dataset,映射将按照字段名称进行
val path = "examples/src/main/resources/people.json"
val peopleDS = spark.read.json(path).as[Person]
peopleDS.show()
// +----+-------+
// | age|   name|
// +----+-------+
// |null|Michael|
// |  30|   Andy|
// |  19| Justin|
// +----+-------+

DataSet与RDD的相互转换

SparkSQL支持两种不同的方式将现有的RDD转化为DataSets,第一个方法是使用反射来反推包含某种类型的RDD的模式(schema)。这种基于反射的方法代码更简洁,在编写Spark应用程序时如果已经知道schema时,这种方法实现起来会更轻松。

创建DataSet的第二种方式是通过编程接口,该接口允许你构建一个schema,然后将schema应用到现有的RDD,虽然这种方式更加麻烦,但它允许你在运行前都不必知道列以及列的类型的情况下来构建DataSet。

1)使用反射推断模式

Spark SQL的Scala接口支持将包含case class的RDD自动转换为DataFrame,case class定义了表的模式,使用反射读取case class参数的名称,并将其作为对应列的名称。case class还可以包含多层嵌套或包含Seqs或Array等复杂类型。这样的RDD可以通过隐式转换为一个DataFrame,然后注册为一个表。表可以在后续的SQL语句中使用。

// 用于从RDD到DataFrame的隐式转换
import spark.implicits._

// 从文本文件中创建一个包含Person对象的RDD,并且将它转换为DataFrame
val peopleDF = spark.sparkContext
  .textFile("examples/src/main/resources/people.txt")
  .map(_.split(","))
  .map(attributes => Person(attributes(0), attributes(1).trim.toInt))
  .toDF()
// 将DataFrame注册为临时视图
peopleDF.createOrReplaceTempView("people")

// 通过spark提供的sql方法可以运行SQL语句
val teenagersDF = spark.sql("SELECT name, age FROM people WHERE age BETWEEN 13 AND 19")

// 结果中的列可以通过字段索引(index)来访问
teenagersDF.map(teenager => "Name: " + teenager(0)).show()
// +------------+
// |       value|
// +------------+
// |Name: Justin|
// +------------+

// 或者根据字段name来访问
teenagersDF.map(teenager => "Name: " + teenager.getAs[String]("name")).show()
// +------------+
// |       value|
// +------------+
// |Name: Justin|
// +------------+

//Dataset[Map[K,V]]没有预定义的编码器,需要明确的定义出来
implicit val mapEncoder = org.apache.spark.sql.Encoders.kryo[Map[String, Any]]
// 基本类型和case类也可以定义为隐式val stringIntMapEncoder: Encoder[Map[String, Any]] = ExpressionEncoder()

//row.getValuesMap[T]可以检索多个列到一个Map[String,T]
teenagersDF.map(teenager => teenager.getValuesMap[Any](List("name","age"))).collect()
// Array(Map("name" -> "Justin", "age" -> 19))

2)编程接口模式

当cass class不能提前定义时(例如记录的结构被编码为一个string或text的dataset被解析,对于不同的用户,字段的投影也不同),可以通过一下三个步骤以编程方式创建DataFrame。

1.从原始RDD中创建RDD[Row]

2.创建由StructType表示的schema,schema要与第一步创建的RDD中的row的行结构相匹配

3.通过SparkSession提供的createDataFrame方法将schema应用到RDD[row]。

下面开始编写示例:

import org.apache.spark.sql.types._

//通过文本文件创建一个RDD  文本中每一行的数据格式为:name age
//例如:zhangsan 16
val peopleRDD = spark.sparkContext.textFile("examples/src/main/resources/people.txt")

val schemaString="name age"
//生成schema
val fields = schemaString.split(" ")
  .map(fieldName => StructField(fieldName, StringType, nullable = true))
val schema = StructType(fields)
//将RDD[String]转为RDD[Row]
val rowRDD = peopleRDD
  .map(_.split(","))
  .map(attributes => Row(attributes(0), attributes(1).trim))
// 将schema应用于schema
val peopleDF = spark.createDataFrame(rowRDD, schema)
//到这里DataFrame就建立完成了,可以像上面一样创建临时视图再进行sql查询了

总结:乍看之下,第二种方式的示例如果用定义cass class+反射的方式会更方便,第二种方式的好处可能大家还没太明白。但实际上第二段示例代码中的schemaString可以是动态的,什么意思呢,我可以在程序调用的时候再根据读取的文件的把schemaString传递进来,包括文件的地址也是可以更改的。这样读取不同结构的文件时,如果使用方法一,有多少种结构就需要写多少遍一样的代码,而对方式二来说只要在运行时更改schemaString与textFile的取值就可以了。但还有一个遗憾是这些结构中的每一行的参数数量必须是一致的。因为.map(attributes => Row(attributes(0), attributes(1).trim))这里我暂时没想到让他也跟着schemaString变化的办法。

聚合

DataFrame内置的函数中提供了常见的聚合函数,如count(),countDistinct(),avg(),max()等,虽然这些方法是设计用来提供给DataFrame使用的,但SparkSQL在Java和Scala中也有类型安全的版本,用于处理强类型DataSet,此外,用户不受预定义聚合函数的限制,可以自由创建自己的聚合函数。

1.非强类型用户自定义聚合函数(提供给DataFrame使用)

用户必须继承UserDefinedAggregateFunction抽象类来实现自定义非强类型聚合函数。例如,可以自定义一个求平均值的average函数,如下所示:

import org.apache.spark.sql.{Row, SparkSession}
import org.apache.spark.sql.expressions.MutableAggregationBuffer
import org.apache.spark.sql.expressions.UserDefinedAggregateFunction
import org.apache.spark.sql.types._

object MyAverage extends UserDefinedAggregateFunction {
  // 此聚合函数的输入参数的数据类型
  def inputSchema: StructType = StructType(StructField("inputColumn", LongType) :: Nil)
  // 聚合缓冲区中值的数据类型
  def bufferSchema: StructType = {
    StructType(StructField("sum", LongType) :: StructField("count", LongType) :: Nil)
  }
  // 返回值的数据类型
  def dataType: DataType = DoubleType
  // 输入相同的情况下这个函数是否总是返回相同的输出
  def deterministic: Boolean = true
  // 初始化给定的聚合缓冲区。缓冲区本身也是一个“Row”,除了检索索引值(如get()、getBoolean())等标准方法之外,还提供了更新其Value的方法。注意,缓冲区中的数组和映射仍然是静态的不可变的
  def initialize(buffer: MutableAggregationBuffer): Unit = {
    buffer(0) = 0L
    buffer(1) = 0L
  }
  // 使用来自“input”的新输入数据更新给定的聚合缓冲区“buffer”
  def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
    if (!input.isNullAt(0)) {
      buffer(0) = buffer.getLong(0) + input.getLong(0)
      buffer(1) = buffer.getLong(1) + 1
    }
  }
  // 合并两个聚合缓冲区并将更新后的缓冲区值存储回“buffer1”
  def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
    buffer1(0) = buffer1.getLong(0) + buffer2.getLong(0)
    buffer1(1) = buffer1.getLong(1) + buffer2.getLong(1)
  }
  // 计算最终结果
  def evaluate(buffer: Row): Double = buffer.getLong(0).toDouble / buffer.getLong(1)
}

// 注册函数来访问它
spark.udf.register("myAverage", MyAverage)

val df = spark.read.json("examples/src/main/resources/employees.json")
df.createOrReplaceTempView("employees")
df.show()
// +-------+------+
// |   name|salary|
// +-------+------+
// |Michael|  3000|
// |   Andy|  4500|
// | Justin|  3500|
// |  Berta|  4000|
// +-------+------+

val result = spark.sql("SELECT myAverage(salary) as average_salary FROM employees")
result.show()
// +--------------+
// |average_salary|
// +--------------+
// |        3750.0|
// +--------------+

2.强类型数据集的用户自定义函数(提供给DataSet使用)

强类型数据集的用户定义聚合函数围绕Aggregator抽象类。例如,类型安全的用户定义平均值可以如下所示:

import org.apache.spark.sql.{Encoder, Encoders, SparkSession}
import org.apache.spark.sql.expressions.Aggregator

case class Employee(name: String, salary: Long)
case class Average(var sum: Long, var count: Long)

object MyAverage extends Aggregator[Employee, Average, Double] {
  // 定义一个对于这个聚合函数的zero value.必须保证对于任意符合格式的b+zero value=b
  def zero: Average = Average(0L, 0L)
  // 将两个值组合生成一个新值。为了提高性能,函数可以修改“buffer”并返回它,而不是构造一个新的对象
  def reduce(buffer: Average, employee: Employee): Average = {
    buffer.sum += employee.salary
    buffer.count += 1
    buffer
  }
  // 合并两个中间值
  def merge(b1: Average, b2: Average): Average = {
    b1.sum += b2.sum
    b1.count += b2.count
    b1
  }
  // 转换归约(reduction)的输出
  def finish(reduction: Average): Double = reduction.sum.toDouble / reduction.count
  // 指定中间值类型的编码器
  def bufferEncoder: Encoder[Average] = Encoders.product
  // 指定最终结果类型的编码器
  def outputEncoder: Encoder[Double] = Encoders.scalaDouble
}

val ds = spark.read.json("examples/src/main/resources/employees.json").as[Employee]
ds.show()
// +-------+------+
// |   name|salary|
// +-------+------+
// |Michael|  3000|
// |   Andy|  4500|
// | Justin|  3500|
// |  Berta|  4000|
// +-------+------+

// 将函数转换为' TypedColumn '并为其命名
val averageSalary = MyAverage.toColumn.name("average_salary")
val result = ds.select(averageSalary)
result.show()
// +--------------+
// |average_salary|
// +--------------+
// |        3750.0|
// +--------------+

这里的例子为了方便,读取的数据源都是文本文件,其实日常我们读取的数据源会有很多种类型,包括关系型或非关系型数据库,Hive等等,最后结果也更多会保存到这些数据源中。所以下一篇博客会展示一个从Mysql等读取数据最后将结果写入到MongoDB中的实例并详细解析。

本篇博客的内容就这些了。发现最近的几篇博客内容都比较多,很多地方完全可以单独拿出来,但我写博客的目的就是为了加强自己的记忆、对相关知识的理解以及方便自己随时回顾旧知识,所以请看到的同学不要太介意。还有真心希望CSDN的博客能出个提取目录的功能,就像word的那种,相信体验会好很多。在博客内容比较多的情况下,一定会很有用!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值