大数据技术之_19_Spark学习_03
第1章 Spark SQL 概述
1.1 什么是 Spark SQL
Spark SQL:http://spark.apache.org/sql/
Spark SQL 是 Spark 用来处理结构化数据的一个模块,它提供了一个编程抽象叫做 DataFrame,并且作为分布式 SQL 查询引擎的作用。
我们已经学习了 Hive,它是将 Hive SQL 转换成 MapReduce 然后提交到集群上执行,大大简化了编写 MapReduce 的程序的复杂性,由于 MapReduce 这种计算模型执行效率比较慢。所以 Spark SQL 的应运而生,它是将 Spark SQL 转换成 RDD,然后提交到集群执行,执行效率非常快!
Spark SQL 的特点:
1、易整合(易集成)
2、统一的数据访问方式
3、兼容 Hive
4、标准的数据连接
Spark SQL我们要学什么?SparkSQL 可以看做是一个转换层,向下对接各种不同的结构化数据源,向上提供不同的数据访问方式。
1.2 RDD vs DataFrames vs DataSet
Spark SQL 的数据抽象
在 SparkSQL 中 Spark 为我们提供了两个新的抽象,分别是 DataFrame 和 DataSet。他们和 RDD 有什么区别呢?首先从版本的产生上来看:RDD(Spark1.0) —> DataFrame(Spark1.3) —> DataSet(Spark1.6)
如果同样的数据都给到这三个数据结构,他们分别计算之后,都会给出相同的结果。不同是的他们的执行效率和执行方式。
在后期的 Spark 版本中,DataSet 会逐步取代 RDD 和 DataFrame 成为唯一的 API 接口。
1.2.1 RDD
RDD 弹性分布式数据集,Spark 计算的基石,为用户屏蔽了底层对数据的复杂抽象和处理,为用户提供了一组方便的数据转换与求值方法。
RDD 是一个懒执行的不可变的可以支持 Lambda 表达式的并行数据集合。
RDD 的最大好处就是简单,API 的人性化程度很高。
RDD 的劣势是性能限制,它是一个 JVM 驻内存对象,这也就决定了存在 GC 的限制和数据增加时 Java 序列化成本的升高。
RDD 例子如下:
1.2.2 DataFrame
与 RDD 类似,DataFrame 也是一个分布式数据容器。然而 DataFrame 更像传统数据库的二维表格,除了数据以外,还记录数据的结构信息,即 schema。同时,与 Hive 类似,DataFrame 也支持嵌套数据类型(struct、array 和 map)。从 API 易用性的角度上看,DataFrame API 提供的是一套高层的关系操作,比函数式的 RDD API 要更加友好,门槛更低。由于与 R 和 Pandas 的 DataFrame 类似,Spark DataFrame 很好地继承了传统单机数据分析的开发体验。
上图直观地体现了 DataFrame 和 RDD 的区别。左侧的 RDD[Person] 虽然以 Person 为类型参数,但 Spark 框架本身不了解 Person 类的内部结构。而右侧的 DataFrame 却提供了详细的结构信息,使得 Spark SQL 可以清楚地知道该数据集中包含哪些列,每列的名称和类型各是什么。DataFrame 多了数据的结构信息,即 schema。RDD 是分布式的Java对象
的集合。DataFrame 是分布式的Row对象
的集合。DataFrame 除了提供了比 RDD 更丰富的算子以外,更重要的特点是提升执行效率、减少数据读取以及执行计划的优化,比如 filter 下推、裁剪等。
DataFrame 是为数据提供了 Schema 的视图。可以把它当做数据库中的一张表来对待。
DataFrame 也是懒执行的。
性能上比 RDD 要高,主要有两方面原因:
(1)定制化内存管理:数据以二进制的方式存在于非堆内存
,节省了大量空间之外,还摆脱了 GC 的限制。
(2)优化的执行计划:查询计划通过 Spark catalyst optimiser 进行优化。
比如下面一个例子:
人口数据分析的示例:
为了说明查询优化,我们来看上图展示的人口数据分析的示例。图中构造了两个 DataFrame,将它们 join 之后又做了一次 filter 操作。如果原封不动地执行这个执行计划,最终的执行效率是不高的。因为 join 是一个代价较大的操作,也可能会产生一个较大的数据集。如果我们能将 filter 下推到 join 下方
,先对 DataFrame 进行过滤,再 join 过滤后的较小的结果集,便可以有效缩短执行时间。而 Spark SQL 的查询优化器正是这样做的。简而言之,逻辑查询计划优化就是一个利用基于关系代数的等价变换,将高成本的操作替换为低成本操作的过程
。
得到的优化执行计划在转换成物理执行计划的过程中,还可以根据具体的数据源的特性将过滤条件下推至数据源内。最右侧的物理执行计划中 filter 之所以消失不见,就是因为融入了用于执行最终的读取操作的表扫描节点内。
对于普通开发者而言,查询优化器的意义在于,即便是经验并不丰富的程序员写出的次优的查询,也可以被尽量转换为高效的形式予以执行
。
DataFrame 的劣势在于在编译期缺少类型安全检查,导致运行时出错
。
1.2.3 DataSet
1)是 DataFrame API 的一个扩展,是 Spark 最新的数据抽象。
2)用户友好的 API 风格,既具有类型安全检查也具有 DataFrame 的查询优化特性。
3)DataSet 支持编解码器,当需要访问非堆上的数据时可以避免反序列化整个对象,提高了效率。
4)样例类被用来在 DataSet 中定义数据的结构信息,样例类中每个属性的名称直接映射到 DataSet 中的字段名称。
5)DataFrame 是 DataSet 的特列,type DataFrame = Dataset[Row] ,所以可以通过 as 方法将 DataFrame 转换为 DataSet。Row 是一个类型,跟 Car、Person 这些的类型一样,所有的表结构信息都用 Row 来表示。
6)DataSet 是强类型的。比如可以有 Dataset[Car],Dataset[Person],DataFrame 只是知道字段,但是不知道字段的类型,所以在执行这些操作的时候是没办法在编译的时候检查是否类型失败的,比如你可以对一个 String 进行减法操作,在执行的时候才报错,而 DataSet 不仅仅知道字段,而且知道字段类型,所以有更严格的错误检查。就跟 JSON 对象和类对象之间的类比。
RDD 让我们能够决定怎么做,而 DataFrame 和 DataSet 让我们决定做什么,控制的粒度不一样。
1.2.4 三者的共性
1、RDD、DataFrame、DataSet 全都是 spark 平台下的分布式弹性数据集,为处理超大型数据提供便利。
2、三者都有惰性机制,在进行创建、转换,如 map 方法时,不会立即执行,只有在遇到 action,如 foreach 时,三者才会开始遍历运算,极端情况下,如果代码里面有创建、转换,但是后面没有在 action 中使用对应的结果,在执行时会被直接跳过。
val sparkconf = new SparkConf().setMaster("local").setAppName("test").set("spark.port.maxRetries","1000")
val spark = SparkSession.builder().config(sparkconf).getOrCreate()
val rdd=spark.sparkContext.parallelize(Seq(("a", 1), ("b", 1), ("a", 1)))
// map 不运行
rdd.map {
line =>
println("运行")
line._1
}
3、三者都会根据 spark 的内存情况自动缓存运算,这样即使数据量很大,也不用担心会内存溢出。
4、三者都有 partition 的概念。
5、三者有许多共同的函数,如 filter,排序等。
6、在对 DataFrame 和 DataSet 进行许多操作都需要这个包进行支持
import spark.implicits._
7、DataFrame 和 DataSet 均可使用模式匹配获取各个字段的值和类型
DataFrame:
testDF.map {
case Row(col1: String, col2: Int) =>
println(col1)
println(col2)
col1
case _=>
""
}
DataSet:
case class Coltest(col1: String, col2: Int)extends Serializable // 定义字段名和类型
testDS.map {
case Coltest(col1: String, col2: Int) =>
println(col1)
println(col2)
col1
case _=>
""
}
1.2.5 三者的区别
RDD:
1、RDD 一般和 spark MLlib 同时使用
2、RDD 不支持 spark sql 操作
DataFrame:
1、与 RDD 和 DataSet 不同,DataFrame 每一行的类型固定为 Row,只有通过解析才能获取各个字段的值,如
testDF.foreach{
line =>
val col1=line.getAs[String]("col1")
val col2=line.getAs[String]("col2")
}
每一列的值没法直接访问
2、DataFrame 与 DataSet 一般与 spark MLlib 同时使用
3、DataFrame 与 DataSet 均支持 spark sql 的操作,比如 select,groupby 之类,还能注册临时表/视窗,进行 sql 语句操作,如
dataDF.createOrReplaceTempView("tmp")
spark.sql("select ROW, DATE from tmp where DATE is not null order by DATE").show(100, false)
4、DataFrame 与 DataSet 支持一些特别方便的保存方式,比如 保存成 csv,可以带上表头,这样每一列的字段名一目了然
// 保存
val saveoptions = Map("header" -> "true", "delimiter" -> "\t", "path" -> "hdfs://hadoop102:9000/test")
datawDF.write.format("com.atguigu.spark.csv").mode(SaveMode.Overwrite).options(saveoptions).save()
// 读取
val options = Map("header" -> "true", "delimiter" -> "\t", "path" -> "hdfs://hadoop102:9000/test")
val datarDF= spark.read.options(options).format("com.atguigu.spark.csv").load()
利用这样的保存方式,可以方便的获得字段名和列的对应,而且分隔符(delimiter)可以自由指定。
DataSet:
DataSet 和 DataFrame 拥有完全相同的成员函数,区别只是每一行的数据类型不同。
DataFrame 也可以叫 Dataset[Row],即每一行的类型是 Row,不解析,每一行究竟有哪些字段,各个字段又是什么类型都无从得知,只能用上面提到的 getAS 方法或者共性中的第七条提到的模式匹配拿出特定字段。
而 DataSet 中,每一行是什么类型是不一定的,在自定义了 case class 之后可以很自由的获得每一行的信息。
case class Coltest(col1: String, col2: Int) extends Serializable // 定义字段名和类型
/**
rdd
("a", 1)
("b", 1)
("a", 1)
**/
val test: Dataset[Coltest] = rdd.map {
line =>
Coltest(line._1, line._2)
}.toDS
test.map{
line =>
println(line.col1)
println(line.col2)
}
可以看出,DataSet 在需要访问列中的某个字段时是非常方便的,然而,如果要写一些适配性很强的函数时,如果使用 DataSet,行的类型又不确定,可能是各种 case class,无法实现适配,这时候用 DataFrame,即 Dataset[Row] 就能比较好的解决问题。
第2章 执行 Spark SQL 查询
2.1 命令行查询流程
打开 spark-shell
例子:查询大于 30 岁的用户
创建如下 JSON 文件,注意 JSON 的格式:
{
"name":"Michael", "age":30}
{
"name":"Andy", "age":30}
{
"name":"Justin", "age":19}
操作步骤如下:
2.2 IDEA 创建 Spark SQL 程序
Spark SQL 在 IDEA 中程序的打包和运行方式都和 Spark Core 类似,Maven 依赖中需要添加新的依赖项:
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.11</artifactId>
<version>${spark.version}</version>
<!-- provided 表示编译期可用,运行期不可用 -->
<!--<scope>provided</scope>-->
</dependency>
程序如下:
package com.atguigu.sparksql
import org.apache.spark.sql.SparkSession
import org.slf4j.LoggerFactory
object HelloWorld {
val logger = LoggerFactory.getLogger(HelloWorld.getClass)
def main(args: Array[String]) {
// 创建 SparkSession 并设置 App 名称
val spark = SparkSession
.builder()
.appName("Spark SQL basic example")
.config("spark.some.config.option", "some-value")
.getOrCreate()
// 通过隐式转换将 RDD 操作添加到 DataFrame 上
import spark.implicits._
// 通过 spark.read 操作读取 JSON 数据
val df = spark.read.json("examples/src/main/resources/people.json")
// show 操作类似于 Action,将 DataFrame 直接打印到 Console 上
df.show()
// DSL 风格的使用方式:属性的获取方法 $
df.filter($"age" > 21).show()
//将 DataFrame 注册为表
df.createOrReplaceTempView("persons")
// 执行 Spark SQL 查询操作
spark.sql("select * from perosns where age > 21").show()
// 关闭资源
spark.stop()
}
}
第3章 Spark SQL 解析
3.1 新的起始点 SparkSession
在老的版本中,SparkSQL 提供两种 SQL 查询起始点,一个叫 SQLContext,用于 Spark 自己提供的 SQL 查询,一个叫 HiveContext,用于连接 Hive 的查询,SparkSession 是 Spark 最新的 SQL 查询起始点,实质上是 SQLContext 和 HiveContext 的组合,所以在 SQLContext 和 HiveContext 上可用的 API 在 SparkSession 上同样是可以使用的。SparkSession 内部封装了 SparkContext,所以计算实际上是由 SparkContext 完成的。
import org.apache.spark.sql.SparkSession
// 创建 SparkSession 并设置 App 名称
val spark = SparkSession
.builder()
.appName("Spark SQL basic example")
.config("spark.some.config.option", "some-value")
.getOrCreate()
// 通过隐式转换将 RDD 操作添加到 DataFrame 上
import spark.implicits._
SparkSession.builder 用于创建一个 SparkSession。
import spark.implicits._ 的引入是用于将 DataFrames 隐式转换成 RDD,使 df 能够使用 RDD 中的方法。
如果需要 Hive 支持,则需要以下创建语句:
import org.apache.spark.sql.SparkSession
val spark = SparkSession
.builder()
.appName("Spark SQL basic example")
.config("spark.some.config.option", "some-value")
.enableHiveSupport()
.getOrCreate()
// For implicit conversions like converting RDDs to DataFrames
import spark.implicits._
3.2 创建 DataFrames
在 Spark SQL 中 SparkSession 是创建 DataFrames 和执行 SQL 的入口,创建 DataFrames 有三种方式,一种是可以从一个存在的 RDD 进行转换,还可以从 Hive Table 进行查询返回,或者通过 Spark 的数据源进行创建。
1、从 Spark 数据源进行创建:
val df = spark.read.json("examples/src/main/resources/people.json")
// Displays the content of the DataFrame to stdout
df.show()
+---+-------+
|age| name|
+---+-------+
| 30|Michael|
| 30| Andy|
| 19| Justin|
+---+-------+
2、从 RDD 进行转换:
/**
Michael, 29
Andy, 30
Justin, 19
**/
scala> val personRdd = sc.textFile("examples/src/main/resources/people.txt")
personRdd: org.apache.spark.rdd.RDD[String] = examples/src/main/resources/people.txt MapPartitionsRDD[18] at textFile at <console>:24
// 把每一行的数据用 "," 隔开,然后通过第二个 map 转换成一个 Array 再通过 toDF 映射给 name 和 age
scala> val personDF3 = personRdd.map(_.split(",")).map(paras => (paras(0).trim(), paras(1).trim().toInt)).toDF("name", "age")
personDF3: org.apache.spark.sql.DataFrame = [name: string, age: int]
scala> personDF3.collect
res0: Array[org.apache.spark.sql.Row] = Array([Michael,29], [Andy,30], [Justin,19])
scala> personDF.show()
+-------+---+
| name|age|
+-------+---+
|Michael| 29|
| Andy| 30|
| Justin| 19|
+-------+---+
3、从 Hive Table 进行查询返回,我们在数据源章节介绍。
3.3 DataFrame 常用操作
3.3.1 DSL 风格语法
// This import is needed to use the $-notation
import spark.implicits._
// Print the schema in a tree format
df.printSchema()
root
|-- age: long (nullable = true)
|-- name: string (nullable = true)
// Select only the "name" column
df.select("name").show()
+-------+
| name|
+-------+
|Michael|
| Andy|
| Justin|
+-------+
// Select everybody, but increment the age by 1
df.select($"name", $"age" + 1).show()
+-------+---------+
| name|(age + 1)|
+-------+---------+
|Michael| 31|
| Andy| 31|
| Justin| 20|
+-------+---------+
// Select person older than 21
df.filter($"age" > 21).show()
+---+-------+
|age| name|
+---+-------+
| 30|Michael|
| 30| Andy|
+---+-------+
// Count person by age
df.groupBy("age").count().show()
+---+-----+
|age|count|
+---+-----+
| 19| 1|
| 30| 2|
+---+-----+
3.3.2 SQL 风格语法
// Register the DataFrame as a SQL temporary view
df.createOrReplaceTempView("persons")
val sqlDF = spark.sql("SELECT * FROM persons")
sqlDF.show()
+---+-------+
|age| name|
+---+-------+
| 30|Michael|
| 30| Andy|
| 19| Justin|
+---+-------+
// Register the DataFrame as a global temporary view
df.createGlobalTempView("persons")
// Global temporary view is tied to a system preserved database `global_temp`
spark.sql("SELECT * FROM global_temp.persons").show()
+---+-------+
|age| name|
+---+-------+
| 30|Michael|
| 30| Andy|
| 19| Justin|
+---+-------+
// Global temporary view is cross-session
spark.newSession().sql("SELECT * FROM global_temp.persons").show()
+---+-------+
|age| name|
+---+-------+
| 30|Michael|
| 30| Andy|
| 19| Justin|
+---+-------+
临时表是 Session 范围内的,Session 退出后,表就失效了。如果想应用范围内有效,可以使用全局表。
注意
:使用全局表时需要全路径访问,如:global_temp.persons
3.4 创建 DataSet
DataSet 是具有强类型的数据集合,需要提供对应的类型信息。
scala> case class Person(name: String, age: Long)
defined class Person
scala> val caseClassDS = Seq(Person("Andy", 32)).toDS()
caseClassDS: org.apache.spark.sql.Dataset[Person] = [name: string, age: bigint]
scala> caseClassDS.show(