1. SparkSession
1.1 新的起点
老版本中,Spark SQL 提供两种 SQL 查询的起点,一个为 SQLContext,用于 Spark 为自己提供的 SQL 查询;另一个为 HiveContext,用于连接 Hive 查询,SparkSession 是 SparkSQL 查询的新起点。实际上是 SQLContext、HiveContext 的组合,所以在 SQLContext 和 HiveContext 上可用的 API 在 SparkSession 上同样是可以使用的。SparkSession 内部封装 SparkContext,所以计算实际上是由 SparkContext 完成的。
如果留心观察,在命令行中启动 Spark 后就可以看到 SparkSession:
1.2 IDEA 中创建 SparkSession
版本说明:
Scala:2.12.15
Spark:3.1.2
Spark Core:2.12
Spark SQL:2.12
1.2.1 添加SparkSQL依赖
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.12</artifactId>
<version>3.1.2</version>
</dependency>
1.2.2 为项目添加 Scala SDK
1.2.3 创建程序入口
SparkSession.builder
用于创建一个SparkSession。
val spark = SparkSession.builder()
.appName("CreateSession")
.master("local[2]")
.getOrCreate()
将 Session 对象命名为
spark
,原因可看上文截图中的 Session 别名。SparkContext 别名为
sc
,SparkSession别名为spark
在后期使用中,会用到RDD,所以可添加隐式转换的包,用于将 DataFrame 转换为 RDD,使 DataFrame 能够使用 RDD 中的方法。
import spark.implicits._
这里的 spark 为上面创建出来的对象。
2. DataFrame
2.1 创建 DataFrame
在 SparkSQL 中,SparkSession 是创建 DataFrame 和执行 SQL 的入口。
创建 DataFrame 有三种方式:
- 通过 Spark 数据源进行创建
- 通过一个存在的 RDD 进行转换
- 使用
spark.createDataFrame
创建 - 通过 Hive Table 进行查询返回
准备数据 employees.json
:
{"name":"Michael", "salary":3000}
{"name":"Andy", "salary":4500}
{"name":"Justin", "salary":3500}
{"name":"Berta", "salary":4000}
2.1.1 通过 Spark 数据源进行创建
val spark = SparkSession.builder()
.appName("DataFrames")
.master("local[2]")
.getOrCreate()
// 创建 DataFrame ==========================================
// 方式一: 通过 Spark 数据源进行创建
val df = spark.read.json("employees.json")
df.show()
2.1.2 通过一个存在的 RDD 进行转换
val spark = SparkSession.builder()
.appName("DataFrames")
.master("local[2]")
.getOrCreate()
import spark.implicits._
// 创建 DataFrame ==========================================
// 方式二: 通过一个存在的 RDD 进行转换
val sc = SparkContext.getOrCreate()
val rdd = sc.textFile("employees.json")
val df = rdd.map(_.split("\""))
.map(items => (items(3), items(6).substring(1, items(6).length - 1)))
.toDF("name", "salary")
df.show()
2.1.3 使用 spark.createDataFrame
创建
DataFrame在Spark1.6之后没有保留单独的类,而是将Dataset[Row]作为DataFrame类型的实现。
2.1.3.1 源码窥探(DF结构)
1️⃣ createDataFrame
方法具有多种重载
2️⃣ 进入到其源码包 SparkSession
种,我们可找打 SparkSQL 对应的的方法:
- rowRDD: RDD[Row]:Row泛型的RDD
- schema: StructType:结构类型
3️⃣ 进入到 Row 中,可以发现 Row 是一个特质,DF存储数据时,需要使用Row的实现类:Generic(通用类)、Aggregation(聚合,用于HDFS),不妨先看第一个 GenericRow
4️⃣ 在该实现类中,包含一个成员属性,类型为 Array[Any]
,由于表中每一行包含不同类型的值,所以使用 Array[Any] 来保存各种类型的值,而我们知道 DF 不仅包含了字段的值,还包含字段的类型,显然不能使用该实现类!
5️⃣ 继续向下,本类中包含了 GenericRow
的实现类,名为 GenericRowWithSchema
,
6️⃣ 在 GenericRowWithSchema
类中不仅包含字段的值,还具有一个成员变量,类型为 StructType, StructType 中包含一个成员属性fields类型是Array[StructField],可用于保存每行所有字段的结构信息,StructType 类如下:
7️⃣ StructField 类具体定义了字段的名称及对应的数据类型,所及实际使用就是该类,如下:
8️⃣ 所以我们可以看到,在 GenericRowWithSchema
中,成员变量为两个数组,一个用于保存数据内容,另一个用于保存字段的名称和属性信息,两个数组对应如下:
Array[Any] ( "JOEl", 18)
Array[StructField](StructField("name",StringType),StructField("age",IntegerType) )
🌟 DF结构表述:
DataFrame 中,包含了数据的内容及数据的结构信息(字段名称、字段类型),DataFrame 就是 DataSet 的一个特例,其泛型为 Row 类型。Row 是一个特质,该特质具有两个特质,一个是
GenericRow
,它的成员变量 values 是一个 Any 泛型的数组,用来保存每一行当中的值;由于 Any 不能保存列的名称和列的类型,所以还有一个GenericRowWithSchema
,其包含一个 StructType 泛型的成员变量 schema,StructType 是 StructField 泛型的数组。StructField 类中包含了四个属性,其中主要的两个属性:name、datatype。两个数组对应后,values数组用来保存那一列的值,fileds 数组用 StructField 保存那一列的字段名称和属性,从而实现映射为别结构!
2.1.3.2 创建 DataFrame
val rdd: RDD[(String, Int)] = sc.makeRDD(List(
("Tom", 18),
("Jack", 28)
))
val rdd1: RDD[Row] = rdd.map(t => {
Row(t._2, t._1)
})
val frame = spark.createDataFrame(
rdd1, // 用于保存每列数据的数组
StructType(
Array[StructField]( // 用于保存字段名、字段类型的数组
StructField("age", IntegerType),
StructField("name", StringType)
)
)
)
frame.show()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oDMzev9b-1640171892837)(https://cdn.jsdelivr.net/gh/JOEL-T99/Pic//img/image-20211222191509112.png)]
列的顺序按照字段名的字段顺序排列!
🌈
protected[xxx]
标识其修饰的类或属性可以在某包下被访问!
2.2 DataFrame 常用操作
2.2.1 SQL 语法风格
SQL 语法风格指的是查询数据时使用 SQL 语句来查询,这个风格相当于查询一张表,但 DataFrame 只是一种数据抽象,所以需要创建临时视图或全局视图来辅助。
全局视图相当于广播变量,将会存储在 Executor 中的 Cache 中,所有 Task 共用一份数据。
val spark = SparkSession.builder()
.appName("DataFrames")
.master("local[2]")
.getOrCreate()
val df = spark.read.json("employees.json")
// 创建或替换临时视图
df.createOrReplaceTempView("employees")
// 使用sql语句查询
val sqlDF = spark.sql("select * from employees where salary > 3500")
// 显示数据
sqlDF.show()
val df = spark.read.json("employees.json")
// 创建全局视图
df.createOrReplaceGlobalTempView("employees")
// 使用sql语句查询,当使用全局表时,需要使用 global_temp.表名
val sqlDF = spark.sql("select * from global_temp.employees where salary > 3500")
// 显示数据
sqlDF.show()
2.2.2 DSL语法风格
DSL(Domain Specific Language):DataFrame 提供的一种领域特定语言去管理结构化的数据,使用 DSL 语法风格就不用创建视图了。
val df = spark.read.json("employees.json")
df
.select("name", "salary")
.where("salary > 3500")
.show()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1ALvfP92-1640171892840)(https://cdn.jsdelivr.net/gh/JOEL-T99/Pic//img/image-20211222132342042.png)]
涉及到运算时,列名前需要使用 $
标识,或使用单引号。(一用则全用)
// 查询时运算一
df
.select($"name", $"salary" + 100 as "salary+100")
.where("salary > 3500")
.show()
// 查询时运算二
df
.select('name, 'salary + 100 as "salary+100")
.where("salary > 3500")
.show()
2.3 DataFrame、RDD 互操作
在 IDEA 开发中,如果需要 RDD、DataFrame、DataSet 之间的互操作,需要引入上文提到的 import spark.implicits._
spark
为 SparkSession 对象!
spark
不能使用var
修饰!因为 Scala 只支持val
修饰的对象引入
2.3.1 DataFrame 转为 RDD
DataFrame 是对 RDD 的封装,所以可以直接获取内部的 RDD。
val df = spark.read.json("employees.json")
val rdd = df.rdd
rdd.foreach(println)
得到的 RDD 泛型为 Row,解析后获取的字段会自动转型
val emp = rdd.collect
println(emp(0)(0).getClass)
println(emp(0)(1).getClass)
/*
输出:
class java.lang.String
class java.lang.Long
*/
2.3.2 RDD 转为 DataFrame
RDD 转为 DataFrame 时,一般通过案例类作为泛型。
case class employees(name: String, scala: Int)
val rdd = sc.makeRDD(List(("Michael",3000),("Andy",4500),("Justin",3500),("Berta",4000)))
val df = rdd
.map(row => employees(row._1, row._2))
.toDF()
df.show()
3. DataSet
DataSet 是具有强类型的数据集合,需要提供对应的类型信息。
3.1 创建 DataSet
创建 DataSet 有三种方式:
- 使用基本类型的序列创建 DataSet
- 使用样例类的序列创建 DataSet
- 读取文件创建 DataSet
3.1.1 使用基本类型的序列创建 DataSet
val ds = Seq(1,2,3,4,5).toDS()
ds.map(_+1).show()
3.1.2 使用样例类的序列创建 DataSet
case class employees(name: String, salary: Int)
val ds = Seq(employees("Michael", 3000),
employees("Andy", 4500),
employees("Justin", 3500),
employees("Berta", 4000)).toDS()
ds.map(_.salary + 100).show()
3.1.3 读取文件创建 DataSet
val ds = spark.read.json("employees.json").as[employees]
ds.show()
3.2 DataSet、RDD 互操作
3.2.1 DataSet 转为 RDD
DataSet 也是对 RDD 的封装,所以可以直接获取内部的 RDD。
val ds = spark.read.json("employees.json").as[employees]
val rdd = ds.rdd
rdd.foreach(println)
3.2.2 RDD 转为 DataSet
SparkSQL 能够自动将包含有 case 类的 RDD 转换成 DataSet,case 类定义了 table 的结构,case 类属性通过反射变成了表的列名。Case 类可以包含诸如 Seq 或者 Array 等复杂的结构。
val rdd = sc.makeRDD(List(("Michael",3000),("Andy",4500),("Justin",3500),("Berta",4000)))
val ds = rdd
.map(row => employees(row._1, row._2))
.toDS()
ds.show()
3.3 DataSet、DataFrame 互操作
DataFrame 其实是DataSet 的特例,所以它们之间是可以互相转换的。
3.3.1 DataSet 转为 DataFrame
val ds = spark.read.json("employees.json").as[employees]
val df = ds.toDF()
df.show()
3.3.2 DataFrame 转为 DataSet
val df = spark.read.json("employees.json")
val ds = df.as[employees]
ds.show()
4. 类型之间的转换总结
DataFrame 转为 RDD:
val rdd = df.rdd
DataSet 转为 RDD:
val rdd = ds.rdd
RDD 转为 DataFrame:
// 无案例类的情况下
import spark.implicits._
val df = rdd
.map(row => (row._1, row._2))
.toDF("col1", "col2")
// 有案例类的情况下
case class employees(name: String, scalary: Int)
import spark.implicits._
val df = rdd
.map(row => employees(row._1, row._2))
.toDF()
RDD 转为 DataSet:
import spark.implicits._
val ds = rdd
.map(row => employees(row._1, row._2))
.toDS()
DataFrame 转为 DataSet:
case class employees(name: String, scalary: Int)
import spark.implicits._
val ds = df.as[employees]
DataSet 转为 DataFrame:
import spark.implicits._
val df = ds.toDF()