SparkSQL
-
SparkSQL
是什么 -
SparkSQL
如何使用
1. SparkSQL 是什么
对于一件事的理解, 应该分为两个大部分, 第一, 它是什么, 第二, 它解决了什么问题
-
理解为什么会有
SparkSQL
-
理解
SparkSQL
所解决的问题, 以及它的使命
1.1. SparkSQL 的出现契机
理解 SparkSQL
是什么
-
历史前提
-
发展过程
-
重要性
因为 SQL
是数据分析领域一个非常重要的范式, 所以 Spark
一直想要支持这种范式, 而伴随着一些决策失误, 这个过程其实还是非常曲折的
-
Hive
-
-
解决的问题
-
-
Hive
实现了SQL on Hadoop
, 使用MapReduce
执行任务 -
简化了
MapReduce
任务
新的问题
-
-
-
Hive
的查询延迟比较高, 原因是使用MapReduce
做调度
-
Shark
-
-
-
解决的问题
-
-
Shark
改写Hive
的物理执行计划, 使用Spark
作业代替MapReduce
执行物理计划 -
使用列式内存存储
-
以上两点使得
Shark
的查询效率很高
新的问题
-
-
-
Shark
重用了Hive
的SQL
解析, 逻辑计划生成以及优化, 所以其实可以认为Shark
只是把Hive
的物理执行替换为了Spark
作业 -
执行计划的生成严重依赖
Hive
, 想要增加新的优化非常困难 -
Hive
使用MapReduce
执行作业, 所以Hive
是进程级别的并行, 而Spark
是线程级别的并行, 所以Hive
中很多线程不安全的代码不适用于Spark
-
由于以上问题,
Shark
维护了Hive
的一个分支, 并且无法合并进主线, 难以为继 -
-
-
解决的问题
-
-
Spark SQL
使用Hive
解析SQL
生成AST
语法树, 将其后的逻辑计划生成, 优化, 物理计划都自己完成, 而不依赖Hive
-
执行计划和优化交给优化器
Catalyst
-
内建了一套简单的
SQL
解析器, 可以不使用HQL
, 此外, 还引入和DataFrame
这样的DSL API
, 完全可以不依赖任何Hive
的组件 -
Shark
只能查询文件,Spark SQL
可以直接降查询作用于RDD
, 这一点是一个大进步
新的问题
-
-
对于初期版本的
SparkSQL
, 依然有挺多问题, 例如只能支持SQL
的使用, 不能很好的兼容命令式, 入口不够统一等
-
-
SparkSQL
在 2.0 时代, 增加了一个新的API
, 叫做Dataset
,Dataset
统一和结合了SQL
的访问和命令式API
的使用, 这是一个划时代的进步在
Dataset
中可以轻易的做到使用SQL
查询并且筛选数据, 然后使用命令式API
进行探索式分析
SparkSQL
Dataset
重要性
|
SparkSQL
是什么
SparkSQL
是一个为了支持 SQL
而设计的工具, 但同时也支持命令式的 API
1.2. SparkSQL 的适用场景
理解 SparkSQL
的适用场景
定义 | 特点 | 举例 | |
---|---|---|---|
结构化数据 | 有固定的 | 有预定义的 | 关系型数据库的表 |
半结构化数据 | 没有固定的 | 没有固定的 | 指一些有结构的文件格式, 例如 |
非结构化数据 | 没有固定 | 没有固定 | 指文档图片之类的格式 |
-
结构化数据
-
一般指数据有固定的
Schema
, 例如在用户表中,name
字段是String
型, 那么每一条数据的name
字段值都可以当作String
来使用+----+--------------+---------------------------+-------+---------+ | id | name | url | alexa | country | +----+--------------+---------------------------+-------+---------+ | 1 | Google | https://www.google.cm/ | 1 | USA | | 2 | 淘宝 | https://www.taobao.com/ | 13 | CN | | 3 | 菜鸟教程 | http://www.runoob.com/ | 4689 | CN | | 4 | 微博 | http://weibo.com/ | 20 | CN | | 5 | Facebook | https://www.facebook.com/ | 3 | USA | +----+--------------+---------------------------+-------+---------+
半结构化数据
-
一般指的是数据没有固定的
Schema
, 但是数据本身是有结构的{ "firstName": "John", "lastName": "Smith", "age": 25, "phoneNumber": [ { "type": "home", "number": "212 555-1234" }, { "type": "fax", "number": "646 555-4567" } ] }
-
没有固定
-
指的是半结构化数据是没有固定的
Schema
的, 可以理解为没有显式指定Schema
比如说一个用户信息的JSON
文件, 第一条数据的phone_num
有可能是String
, 第二条数据虽说应该也是String
, 但是如果硬要指定为BigInt
, 也是有可能的
因为没有指定Schema
, 没有显式的强制的约束
有结构
-
虽说半结构化数据是没有显式指定
Schema
的, 也没有约束, 但是半结构化数据本身是有有隐式的结构的, 也就是数据自身可以描述自身
例如JSON
文件, 其中的某一条数据是有字段这个概念的, 每个字段也有类型的概念, 所以说JSON
是可以描述自身的, 也就是数据本身携带有元信息
Schema
-
-
-
Spark
的RDD
主要用于处理 非结构化数据 和 半结构化数据 -
SparkSQL
主要用于处理 结构化数据
-
-
-
SparkSQL
提供了更好的外部数据源读写支持-
因为大部分外部数据源是有结构化的, 需要在
RDD
之外有一个新的解决方案, 来整合这些结构化数据源
-
-
SparkSQL
提供了直接访问列的能力-
因为
SparkSQL
主要用做于处理结构化数据, 所以其提供的API
具有一些普通数据库的能力
-
-
SparkSQL
处理什么数据的问题?
SparkSQL
相较于
RDD
的优势在哪?
SparkSQL
适用于什么场景?
SparkSQL
适用于处理结构化数据的场景
-
SparkSQL
是一个即支持SQL
又支持命令式数据处理的工具 -
SparkSQL
的主要适用场景是处理结构化数据
2. SparkSQL 初体验
-
了解
SparkSQL
的API
由哪些部分组成
2.3. RDD 版本的 WordCount
val config = new SparkConf().setAppName("ip_ana").setMaster("local[6]")
val sc = new SparkContext(config)
sc.textFile(“hdfs://node01:8020/dataset/wordcount.txt”)
.flatMap(.split(" "))
.map((, 1))
.reduceByKey(_ + _)
.collect
-
RDD
版本的代码有一个非常明显的特点, 就是它所处理的数据是基本类型的, 在算子中对整个数据进行处理
2.2. 命令式 API 的入门案例
case class People(name: String, age: Int)
val spark: SparkSession = new sql.SparkSession.Builder() (1)
.appName(“hello”)
.master(“local[6]”)
.getOrCreate()
import spark.implicits._
val peopleRDD: RDD[People] = spark.sparkContext.parallelize(Seq(People(“zhangsan”, 9), People(“lisi”, 15)))
val peopleDS: Dataset[People] = peopleRDD.toDS() (2)
val teenagers: Dataset[String] = peopleDS.where('age > 10) (3)
.where('age < 20)
.select('name)
.as[String]
/*
±—+
|name|
±—+
|lisi|
±—+
*/
teenagers.show()
1 | SparkSQL 中有一个新的入口点, 叫做 SparkSession |
2 | SparkSQL 中有一个新的类型叫做 Dataset |
3 | SparkSQL 有能力直接通过字段名访问数据集, 说明 SparkSQL 的 API 中是携带 Schema 信息的 |
// DataFrame
df.groupBy(“age”).count(“age”)
通过上面的代码, 可以清晰的看到, SparkSQL
的命令式操作相比于 RDD
来说, 可以直接通过 Schema
信息来访问其中某个字段, 非常的方便
2.2. SQL 版本 WordCount
val spark: SparkSession = new sql.SparkSession.Builder()
.appName("hello")
.master("local[6]")
.getOrCreate()
import spark.implicits._
val peopleRDD: RDD[People] = spark.sparkContext.parallelize(Seq(People(“zhangsan”, 9), People(“lisi”, 15)))
val peopleDS: Dataset[People] = peopleRDD.toDS()
peopleDS.createOrReplaceTempView(“people”)
val teenagers: DataFrame = spark.sql(“select name from people where age > 10 and age < 20”)
/*
±—+
|name|
±—+
|lisi|
±—+
*/
teenagers.show()
以往使用 SQL
肯定是要有一个表的, 在 Spark
中, 并不存在表的概念, 但是有一个近似的概念, 叫做 DataFrame
, 所以一般情况下要先通过 DataFrame
或者 Dataset
注册一张临时表, 然后使用 SQL
操作这张临时表
SparkSQL
提供了 SQL
和 命令式 API
两种不同的访问结构化数据的形式, 并且它们之间可以无缝的衔接
命令式 API
由一个叫做 Dataset
的组件提供, 其还有一个变形, 叫做 DataFrame
3. [扩展] Catalyst 优化器
-
理解
SparkSQL
和以RDD
为代表的SparkCore
最大的区别 -
理解优化器的运行原理和作用
3.1. RDD 和 SparkSQL 运行时的区别
-
-
大致运行步骤
-
先将
RDD
解析为由Stage
组成的DAG
, 后将Stage
转为Task
直接运行
问题
-
任务会按照代码所示运行, 依赖开发者的优化, 开发者的会在很大程度上影响运行效率
解决办法
-
创建一个组件, 帮助开发者修改和优化代码, 但是这在
RDD
上是无法实现的
为什么
-
-
-
RDD
没有Schema
信息 -
RDD
可以同时处理结构化和非结构化的数据
-
-
和
RDD
不同,SparkSQL
的Dataset
和SQL
并不是直接生成计划交给集群执行, 而是经过了一个叫做Catalyst
的优化器, 这个优化器能够自动帮助开发者优化代码也就是说, 在
SparkSQL
中, 开发者的代码即使不够优化, 也会被优化为相对较好的形式去执行-
为什么
-
首先,
SparkSQL
大部分情况用于处理结构化数据和半结构化数据, 所以SparkSQL
可以获知数据的Schema
, 从而根据其Schema
来进行优化
SparkSQL
提供了这种能力? -
RDD
的运行流程
RDD
无法自我优化?
SparkSQL
提供了什么?
3.2. Catalyst
为了解决过多依赖
|
-
Step 1 : 解析
-
-
-
score.id → id#1#L
为score.id
生成id
为 1, 类型是Long
-
score.math_score → math_score#2#L
为score.math_score
生成id
为 2, 类型为Long
-
people.id → id#3#L
为people.id
生成id
为 3, 类型为Long
-
people.age → age#4#L
为people.age
生成id
为 4, 类型为Long
Step 3 : 对已经加入元数据的
-
-
-
谓词下推
Predicate Pushdown
, 将Filter
这种可以减小数据集的操作下推, 放在Scan
的位置, 这样可以减少操作时候的数据量
-
列值裁剪
Column Pruning
, 在谓词下推后,people
表之上的操作只用到了id
列, 所以可以把其它列裁剪掉, 这样可以减少处理的数据量, 从而优化处理速度
-
还有其余很多优化点, 大概一共有一二百种, 随着
SparkSQL
的发展, 还会越来越多, 感兴趣的同学可以继续通过源码了解, 源码在org.apache.spark.sql.catalyst.optimizer.Optimizer
Step 4 : 上面的过程生成的
-
-
-
在生成`物理计划`的时候, 会经过`成本模型`对整棵树再次执行优化, 选择一个更好的计划
-
在生成`物理计划`以后, 因为考虑到性能, 所以会使用代码生成, 在机器中运行
-
SQL
, 并且生成
AST
(抽象语法树)
AST
中加入元数据信息, 做这一步主要是为了一些优化, 例如
col = col
这样的条件, 下图是一个简略图, 便于理解
AST
, 输入优化器, 进行优化, 从两种常见的优化开始, 简单介绍
AST
其实最终还没办法直接运行, 这个
AST
叫做
逻辑计划
, 结束后, 需要生成
物理计划
, 从而生成
RDD
来运行
queryExecution 方法查看逻辑执行计划, 使用
explain 方法查看物理执行计划
Spark WebUI 进行查看
|
SparkSQL
和 RDD
不同的主要点是在于其所操作的数据是结构化的, 提供了对数据更强的感知和分析能力, 能够对代码进行更深层的优化, 而这种能力是由一个叫做 Catalyst
的优化器所提供的
Catalyst
的主要运作原理是分为三步, 先对 SQL
或者 Dataset
的代码解析, 生成逻辑计划, 后对逻辑计划进行优化, 再生成物理计划, 最后生成代码到集群中以 RDD
的形式运行
4. Dataset 的特点
-
理解
Dataset
是什么 -
理解
Dataset
的特性
Dataset
是什么?
import spark.implicits._
val dataset: Dataset[People] = spark.createDataset(Seq(People(“zhangsan”, 9), People(“lisi”, 15)))
// 方式1: 通过对象来处理
dataset.filter(item => item.age > 10).show()
// 方式2: 通过字段来处理
dataset.filter('age > 10).show()
// 方式3: 通过类似SQL的表达式来处理
dataset.filter(“age > 10”).show()
-
问题1:
-
People
是一个强类型的类
问题2: 这个
-
非常明显是的, 因为
People
对象中有结构信息, 例如字段名和字段类型
问题3: 这个
-
当然可以, 已经演示过了
问题4:
-
Dataset
是一个强类型, 并且类型安全的数据容器, 并且提供了结构化查询API
和类似RDD
一样的命令式API
People
是什么?
Dataset
中是结构化的数据吗?
Dataset
能够使用类似
SQL
这样声明式结构化查询语句的形式来查询吗?
Dataset
是什么?
Dataset
的命令式
API
, 执行计划也依然会被优化
== Parsed Logical Plan ==
'Filter ('id = 0)
± Range (0, 1, splits=8)
== Analyzed Logical Plan ==
id: bigint
Filter (id#51L = cast(0 as bigint))
± Range (0, 1, splits=8)
== Optimized Logical Plan ==
Filter (id#51L = 0)
± Range (0, 1, splits=8)
== Physical Plan ==
*Filter (id#51L = 0)
± *Range (0, 1, splits=8)
Dataset
的底层是什么?
Dataset
对应的
RDD
表示
/*
(2) MapPartitionsRDD[3] at rdd at Testing.scala:159 []
| MapPartitionsRDD[2] at rdd at Testing.scala:159 []
| MapPartitionsRDD[1] at rdd at Testing.scala:159 []
| ParallelCollectionRDD[0] at rdd at Testing.scala:159 []
*/
(1)
println(dataset.rdd.toDebugString) // 这段代码的执行计划为什么多了两个步骤?
/*
(2) MapPartitionsRDD[5] at toRdd at Testing.scala:160 []
| ParallelCollectionRDD[4] at toRdd at Testing.scala:160 []
*/
(2)
println(dataset.queryExecution.toRdd.toDebugString)
1 | 使用 Dataset.rdd 将 Dataset 转为 RDD 的形式 |
2 | Dataset 的执行计划底层的 RDD |
可以看到 (1)
对比 (2)
对了两个步骤, 这两个步骤的本质就是将 Dataset
底层的 InternalRow
转为 RDD
中的对象形式, 这个操作还是会有点重的, 所以慎重使用 rdd
属性来转换 Dataset
为 RDD
-
Dataset
是一个新的Spark
组件, 其底层还是RDD
-
Dataset
提供了访问对象中某个特定字段的能力, 不用像RDD
一样每次都要针对整个对象做操作 -
Dataset
和RDD
不同, 如果想把Dataset[T]
转为RDD[T]
, 则需要对Dataset
底层的InternalRow
做转换, 是一个比价重量级的操作
5. DataFrame 的作用和常见操作
-
理解
DataFrame
是什么 -
理解
DataFrame
的常见操作
DataFrame
是什么?
import spark.implicits._
val peopleDF: DataFrame = Seq(People(“zhangsan”, 15), People(“lisi”, 15)).toDF()
/*
±–±----+
|age|count|
±–±----+
| 15| 2|
±–±----+
*/
peopleDF.groupBy('age)
.count()
.show()
DataFrame
// 必须要导入隐式转换
// 注意: spark 在此处不是包, 而是 SparkSession 对象
import spark.implicits._
val peopleDF: DataFrame = Seq(People(“zhangsan”, 15), People(“lisi”, 15)).toDF()
根据源码可以知道, toDF
方法可以在 RDD
和 Seq
中使用
通过集合创建 DataFrame
的时候, 集合中不仅可以包含样例类, 也可以只有普通数据类型, 后通过指定列名来创建
val spark: SparkSession = new sql.SparkSession.Builder()
.appName("hello")
.master("local[6]")
.getOrCreate()
import spark.implicits._
val df1: DataFrame = Seq(“nihao”, “hello”).toDF(“text”)
/*
±----+
| text|
±----+
|nihao|
|hello|
±----+
*/
df1.show()
val df2: DataFrame = Seq((“a”, 1), (“b”, 1)).toDF(“word”, “count”)
/*
±—±----+
|word|count|
±—±----+
| a| 1|
| b| 1|
±—±----+
*/
df2.show()
DataFrame
val df = spark.read
.option(“header”, true)
.csv(“dataset/BeijingPM20100101_20151231.csv”)
df.show(10)
df.printSchema()
不仅可以从 csv
文件创建 DataFrame
, 还可以从 Table
, JSON
, Parquet
等中创建 DataFrame
, 后续会有单独的章节来介绍
DataFrame
上可以使用的常规操作
val df = spark.read
.option(“header”, true)
.csv(“dataset/BeijingPM20100101_20151231.csv”)
df.printSchema()
Step 2: 对于大部分计算来说, 可能不会使用所有的列, 所以可以选择其中某些重要的列...
df.select('year, 'month, 'PM_Dongsi)
Step 3: 可以针对某些列进行分组, 后对每组数据通过函数做聚合...
df.select('year, 'month, 'PM_Dongsi)
.where('PM_Dongsi =!= “Na”)
.groupBy('year, 'month)
.count()
.show()
SQL
操作
DataFrame
val df = spark.read
.option(“header”, true)
.csv(“dataset/BeijingPM20100101_20151231.csv”)
df.createOrReplaceTempView(“temp_table”)
spark.sql(“select year, month, count(*) from temp_table where PM_Dongsi != ‘NA’ group by year, month”)
.show()
-
DataFrame
是一个类似于关系型数据库表的函数式组件 -
DataFrame
一般处理结构化数据和半结构化数据 -
DataFrame
具有数据对象的 Schema 信息 -
可以使用命令式的
API
操作DataFrame
, 同时也可以使用SQL
操作DataFrame
-
DataFrame
可以由一个已经存在的集合直接创建, 也可以读取外部的数据源来创建
6. Dataset 和 DataFrame 的异同
-
理解
Dataset
和DataFrame
之间的关系
DataFrame
就是
Dataset
DataFrame
和
Dataset
所表达的语义不同
import spark.implicits._
val df: DataFrame = Seq(People(“zhangsan”, 15), People(“lisi”, 15)).toDF() (1)
val ds: Dataset[People] = Seq(People(“zhangsan”, 15), People(“lisi”, 15)).toDS() (2)
1 | DataFrame 就是 Dataset[Row] |
2 | Dataset 的范型可以是任意类型 |
DataFrame
的操作方式和
Dataset
是一样的, 但是对于强类型操作而言, 它们处理的类型不同
DataFrame
在进行强类型操作时候, 例如 map
算子, 其所处理的数据类型永远是 Row
df.map( (row: Row) => Row(row.get(0), row.getAs[Int](1) * 10) )(RowEncoder.apply(df.schema)).show()
但是对于 Dataset
来讲, 其中是什么类型, 它就处理什么类型
ds.map( (item: People) => People(item.name, item.age * 10) ).show()
DataFrame
只能做到运行时类型检查,
Dataset
能做到编译和运行时都有类型检查
-
DataFrame
中存放的数据以Row
表示, 一个Row
代表一行数据, 这和关系型数据库类似 -
DataFrame
在进行map
等操作的时候,DataFrame
不能直接使用Person
这样的Scala
对象, 所以无法做到编译时检查 -
Dataset
表示的具体的某一类对象, 例如Person
, 所以再进行map
等操作的时候, 传入的是具体的某个Scala
对象, 如果调用错了方法, 编译时就会被检查出来
val ds: Dataset[People] = Seq(People("zhangsan", 15), People("lisi", 15)).toDS()
ds.map(person => person.hello) (1)
1 | 这行代码明显报错, 无法通过编译 |
Row
是什么?
// 同样一个对象, 还可以通过一个 Row 对象来表示
val row = Row(“zhangsan”, 10)
// 获取 Row 中的内容
println(row.get(1))
println(row(1))
// 获取时可以指定类型
println(row.getAsInt)
// 同时 Row 也是一个样例类, 可以进行 match
row match {
case Row(name, age) => println(name, age)
}
DataFrame
和
Dataset
之间可以非常简单的相互转换
val spark: SparkSession = new sql.SparkSession.Builder()
.appName("hello")
.master("local[6]")
.getOrCreate()
import spark.implicits._
val df: DataFrame = Seq(People(“zhangsan”, 15), People(“lisi”, 15)).toDF()
val ds_fdf: Dataset[People] = df.as[People]
val ds: Dataset[People] = Seq(People(“zhangsan”, 15), People(“lisi”, 15)).toDS()
val df_fds: DataFrame = ds.toDF()
-
DataFrame
就是Dataset
, 他们的方式是一样的, 也都支持API
和SQL
两种操作方式 -
DataFrame
只能通过表达式的形式, 或者列的形式来访问数据, 只有Dataset
支持针对于整个对象的操作 -
DataFrame
中的数据表示为Row
, 是一个行的概念
7. 数据读写
-
理解外部数据源的访问框架
-
掌握常见的数据源读写方式
7.1. 初识 DataFrameReader
-
理解
DataFrameReader
的整体结构和组成
SparkSQL
的一个非常重要的目标就是完善数据读取, 所以 SparkSQL
中增加了一个新的框架, 专门用于读取外部数据源, 叫做 DataFrameReader
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.DataFrameReader
val spark: SparkSession = …
val reader: DataFrameReader = spark.read
DataFrameReader
由如下几个组件组成
组件 | 解释 |
---|---|
| 结构信息, 因为 |
| 连接外部数据源的参数, 例如 |
| 外部数据源的格式, 例如 |
DataFrameReader
有两种访问方式, 一种是使用 load
方法加载, 使用 format
指定加载格式, 还有一种是使用封装方法, 类似 csv
, json
, jdbc
等
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.DataFrame
val spark: SparkSession = …
// 使用 load 方法
val fromLoad: DataFrame = spark
.read
.format(“csv”)
.option(“header”, true)
.option(“inferSchema”, true)
.load(“dataset/BeijingPM20100101_20151231.csv”)
// Using format-specific load operator
val fromCSV: DataFrame = spark
.read
.option(“header”, true)
.option(“inferSchema”, true)
.csv(“dataset/BeijingPM20100101_20151231.csv”)
但是其实这两种方式本质上一样, 因为类似 csv
这样的方式只是 load
的封装
如果使用 也就是说, |
-
使用
spark.read
可以获取 SparkSQL 中的外部数据源访问框架DataFrameReader
-
DataFrameReader
有三个组件format
,schema
,option
-
DataFrameReader
有两种使用方式, 一种是使用load
加format
指定格式, 还有一种是使用封装方法csv
,json
等
7.2. 初识 DataFrameWriter
-
理解
DataFrameWriter
的结构
对于 ETL
来说, 数据保存和数据读取一样重要, 所以 SparkSQL
中增加了一个新的数据写入框架, 叫做 DataFrameWriter
val spark: SparkSession = ...
val df = spark.read
.option(“header”, true)
.csv(“dataset/BeijingPM20100101_20151231.csv”)
val writer: DataFrameWriter[Row] = df.write
DataFrameWriter
中由如下几个部分组成
组件 | 解释 |
---|---|
| 写入目标, 文件格式等, 通过 |
| 写入模式, 例如一张表已经存在, 如果通过 |
| 外部参数, 例如 |
| 类似 |
| 类似 |
| 用于排序的列, 通过 |
mode
指定了写入模式, 例如覆盖原数据集, 或者向原数据集合中尾部添加等
Scala 对象表示 | 字符串表示 | 解释 |
---|---|---|
|
| 将 |
|
| 将 |
|
| 将 |
|
| 将 |
DataFrameWriter
也有两种使用方式, 一种是使用 format
配合 save
, 还有一种是使用封装方法, 例如 csv
, json
, saveAsTable
等
val spark: SparkSession = ...
val df = spark.read
.option(“header”, true)
.csv(“dataset/BeijingPM20100101_20151231.csv”)
// 使用 save 保存, 使用 format 设置文件格式
df.write.format(“json”).save(“dataset/beijingPM”)
// 使用 json 保存, 因为方法是 json, 所以隐含的 format 是 json
df.write.json(“dataset/beijingPM1”)
默认没有指定 |
-
类似
DataFrameReader
,Writer
中也有format
,options
, 另外schema
是包含在DataFrame
中的 -
DataFrameWriter
中还有一个很重要的概念叫做mode
, 指定写入模式, 如果目标集合已经存在时的行为 -
DataFrameWriter
可以将数据保存到Hive
表中, 所以也可以指定分区和分桶信息
7.3. 读写 Parquet 格式文件
-
理解
Spark
读写Parquet
文件的语法 -
理解
Spark
读写Parquet
文件的时候对于分区的处理
-
什么时候会用到
- 使用代码读写
Parquet
?
Parquet
文件
val df = spark.read
.option(“header”, value = true)
.csv(“dataset/911.csv”)
// 保存 Parquet 文件
df.write.mode(“override”).save(“dataset/911.parquet”)
// 读取 Parquet 文件
val dfFromParquet = spark.read.parquet(“dataset/911.parquet”)
dfFromParquet.createOrReplaceTempView(“911”)
spark.sql(“select * from 911 where zip > 19000 and zip < 19400”).show()
写入Parquet
的时候可以指定分区
// 从 CSV 中读取内容
val dfFromParquet = spark.read.option(“header”, value = true).csv(“dataset/BeijingPM20100101_20151231.csv”)
// 保存为 Parquet 格式文件, 不指定 format 默认就是 Parquet
dfFromParquet.write.partitionBy(“year”, “month”).save(“dataset/beijing_pm”)
这个地方指的分区是类似 |
-
分区发现
val partDF = spark.read.load(“dataset/beijing_pm/year=2010/month=1”) (1)
partDF.printSchema()
1 | 把分区的数据集中的某一个区单做一整个数据集读取, 没有分区信息, 自然也不会进行分区发现 |
val df = spark.read.load("dataset/beijing_pm") (1)
df.printSchema()
1 | 此处读取的是整个数据集, 会进行分区发现, DataFrame 中会包含分去列 |
配置 | 默认值 | 含义 |
---|---|---|
|
| 一些其他 |
|
| 一些其他 |
|
| 打开 Parquet 元数据的缓存, 可以加快查询静态数据 |
|
| 压缩方式, 可选 |
|
| 当为 true 时, Parquet 数据源会合并从所有数据文件收集的 Schemas 和数据, 因为这个操作开销比较大, 所以默认关闭 |
|
| 如果为 |
-
Spark
不指定format
的时候默认就是按照Parquet
的格式解析文件 -
Spark
在读取Parquet
文件的时候会自动的发现Parquet
的分区和分区字段 -
Spark
在写入Parquet
文件的时候如果设置了分区字段, 会自动的按照分区存储
7.4. 读写 JSON 格式文件
-
理解
JSON
的使用场景 -
能够使用
Spark
读取处理JSON
格式文件
-
什么时候会用到
- 读写
JSON
?
JSON
文件
val dfFromParquet = spark.read.load(“dataset/beijing_pm”)
// 将 DataFrame 保存为 JSON 格式的文件
dfFromParquet.repartition(1) (1)
.write.format(“json”)
.save(“dataset/beijing_pm_json”)
1 | 如果不重新分区, 则会为 DataFrame 底层的 RDD 的每个分区生成一个文件, 为了保持只有一个输出文件, 所以重新分区 |
保存为
beijing_pm.jsonl
|
也可以通过 DataFrameReader
读取一个 JSON Line
文件
val spark: SparkSession = ...
val dfFromJSON = spark.read.json(“dataset/beijing_pm_json”)
dfFromJSON.show()
JSON
格式的文件是有结构信息的, 也就是 JSON
中的字段是有类型的, 例如 "name": "zhangsan"
这样由双引号包裹的 Value
, 就是字符串类型, 而 "age": 10
这种没有双引号包裹的就是数字类型, 当然, 也可以是布尔型 "has_wife": true
Spark
读取 JSON Line
文件的时候, 会自动的推断类型信息
val spark: SparkSession = ...
val dfFromJSON = spark.read.json(“dataset/beijing_pm_json”)
dfFromJSON.printSchema()
Spark
可以从一个保存了
JSON
格式字符串的
Dataset[String]
中读取
JSON
信息, 转为
DataFrame
import spark.implicits._
val peopleDataset = spark.createDataset(
“”"{“name”:“Yin”,“address”:{“city”:“Columbus”,“state”:“Ohio”}}""" :: Nil)
spark.read.json(peopleDataset).show()
-
JSON
通常用于系统间的交互,Spark
经常要读取JSON
格式文件, 处理, 放在另外一处 -
使用
DataFrameReader
和DataFrameWriter
可以轻易的读取和写入JSON
, 并且会自动处理数据类型信息
7.5. 访问 Hive
-
整合
SparkSQL
和Hive
, 使用Hive
的MetaStore
元信息库 -
使用
SparkSQL
查询Hive
表 -
案例, 使用常见
HiveSQL
-
写入内容到
Hive
表
7.5.1. SparkSQL 整合 Hive
-
开启
Hive
的MetaStore
独立进程 -
整合
SparkSQL
和Hive
的MetaStore
和一个文件格式不同, Hive
是一个外部的数据存储和查询引擎, 所以如果 Spark
要访问 Hive
的话, 就需要先整合 Hive
-
整合什么 ?
- 为什么要开启
Hive
的
MetaStore
Hive
开启
MetaStore
<property>
<name>javax.jdo.option.ConnectionURL</name>
<value>jdbc:mysql://node01:3306/hive?createDatabaseIfNotExist=true</value>
</property>
<property>
<name>javax.jdo.option.ConnectionDriverName</name>
<value>com.mysql.jdbc.Driver</value>
</property>
<property>
<name>javax.jdo.option.ConnectionUserName</name>
<value>username</value>
</property>
<property>
<name>javax.jdo.option.ConnectionPassword</name>
<value>password</value>
</property>
<property>
<name>hive.metastore.local</name>
<value>false</value>
</property>
<property>
<name>hive.metastore.uris</name>
<value>thrift://node01:9083</value> //当前服务器
</property>
Step 2
: 启动
Hive MetaStore
nohup /export/servers/hive/bin/hive --service metastore 2>&1 >> /var/log.log &
SparkSQL
整合
Hive
的
MetaStore
scp -r /export/servers/spark/conf node02:/export/servers/spark/conf
scp -r /export/servers/spark/conf node03:/export/servers/spark/conf
1 | Spark 需要 hive-site.xml 的原因是, 要读取 Hive 的配置信息, 主要是元数据仓库的位置等信息 |
2 | Spark 需要 core-site.xml 的原因是, 要读取安全有关的配置 |
3 | Spark 需要 hdfs-site.xml 的原因是, 有可能需要在 HDFS 中放置表文件, 所以需要 HDFS 的配置 |
如果不希望通过拷贝文件的方式整合 Hive, 也可以在 SparkSession 启动的时候, 通过指定 Hive 的 MetaStore 的位置来访问, 但是更推荐整合的方式 |
7.5.2. 访问 Hive 表
-
在
Hive
中创建表 -
使用
SparkSQL
访问Hive
中已经存在的表 -
使用
SparkSQL
创建Hive
表 -
使用
SparkSQL
修改Hive
表中的数据
-
在
Hive
中创建表
USE spark_integrition;
CREATE EXTERNAL TABLE student
(
name STRING,
age INT,
gpa string
)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY ‘\t’
LINES TERMINATED BY ‘\n’
STORED AS TEXTFILE
LOCATION ‘/dataset/hive’;
LOAD DATA INPATH ‘/dataset/studenttab10k’ OVERWRITE INTO TABLE student;
通过SparkSQL
查询
Hive
的表
SparkSQL
创建
Hive
表
spark.sql(“CREATE DATABASE IF NOT EXISTS spark_integrition1”)
spark.sql(“USE spark_integrition1”)
spark.sql(createTableStr)
spark.sql(“LOAD DATA INPATH ‘/dataset/studenttab10k’ OVERWRITE INTO TABLE student”)
spark.sql(“select * from student limit”).show()
目前 SparkSQL
支持的文件格式有 sequencefile
, rcfile
, orc
, parquet
, textfile
, avro
, 并且也可以指定 serde
的名称
SparkSQL
处理数据并保存进 Hive 表
val studentDF = spark.read
.option(“delimiter”, “\t”)
.schema(schema)
.csv(“dataset/studenttab10k”)
val resultDF = studentDF.where(“age < 50”)
resultDF.write.mode(SaveMode.Overwrite).saveAsTable(“spark_integrition1.student”) (1)
1 | 通过 mode 指定保存模式, 通过 saveAsTable 保存数据到 Hive |
7.6. JDBC
-
通过
SQL
操作MySQL
的表 -
将数据写入
MySQL
的表中
-
准备
MySQL
环境
USE spark_test;
CREATE TABLE IF NOT EXISTS student
(
id
INT AUTO_INCREMENT,
name
VARCHAR(100) NOT NULL,
age
INT NOT NULL,
gpa
FLOAT,
PRIMARY KEY ( id
)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
SparkSQL
向
MySQL
中写入数据
val schema = StructType(
List(
StructField(“name”, StringType),
StructField(“age”, IntegerType),
StructField(“gpa”, FloatType)
)
)
val studentDF = spark.read
.option(“delimiter”, “\t”)
.schema(schema)
.csv(“dataset/studenttab10k”)
studentDF.write.format(“jdbc”).mode(SaveMode.Overwrite)
.option(“url”, “jdbc:mysql://node01:3306/spark_test”)
.option(“dbtable”, “student”)
.option(“user”, “spark”)
.option(“password”, “Spark123!”)
.save()
MySQL
中读取数据
val connectionProperties = new Properties()
connectionProperties.setProperty(“user”, “spark”)
connectionProperties.setProperty(“password”, “Spark123!”)
spark.read
.jdbc(
url = “jdbc:mysql://node01:3306/spark_test”,
table = “student”,
predicates = predicates,
connectionProperties = connectionProperties
)
.show()
SparkSQL
中并没有直接提供按照 SQL
进行筛选读取数据的 API
和参数, 但是可以通过 dbtable
来曲线救国, dbtable
指定目标表的名称, 但是因为 dbtable
中可以编写 SQL
, 所以使用子查询即可做到
spark.read.format("jdbc")
.option("url", "jdbc:mysql://node01:3306/spark_test")
.option("dbtable", "(select name, age from student where age > 10 and age < 20) as stu")
.option("user", "spark")
.option("password", "Spark123!")
.option("partitionColumn", "age")
.option("lowerBound", 1)
.option("upperBound", 60)
.option("numPartitions", 10)
.load()
.show()
8. Dataset (DataFrame) 的基础操作
这一章节主要目的是介绍 Dataset
的基础操作, 当然, DataFrame
就是 Dataset
, 所以这些操作大部分也适用于 DataFrame
-
有类型的转换操作
-
无类型的转换操作
-
基础
Action
-
空值如何处理
-
统计操作
8.1. 有类型操作
分类 | 算子 | 解释 |
---|---|---|
转换 |
|
通过
|
|
| |
|
| |
|
| |
|
|
val structType = StructType(
Seq(
StructField(“name”, StringType),
StructField(“age”, IntegerType),
StructField(“gpa”, FloatType)
)
)
val sourceDF = spark.read
.schema(structType)
.option(“delimiter”, “\t”)
.csv(“dataset/studenttab10k”)
val dataset = sourceDF.as[Student]
dataset.show()
过滤
filter
filter
用来按照条件过滤数据集
import spark.implicits._
val ds = Seq(Person("zhangsan", 15), Person("lisi", 15)).toDS()
ds.filter( person => person.name == "lisi" ).show()
聚合
groupByKey
grouByKey
算子的返回结果是 KeyValueGroupedDataset
, 而不是一个 Dataset
, 所以必须要先经过 KeyValueGroupedDataset
中的方法进行聚合, 再转回 Dataset
, 才能使用 Action
得出结果
其实这也印证了分组后必须聚合的道理
import spark.implicits._
val ds = Seq(Person("zhangsan", 15), Person("zhangsan", 15), Person("lisi", 15)).toDS()
ds.groupByKey( person => person.name ).count().show()
切分
randomSplit
randomSplit
会按照传入的权重随机将一个 Dataset
分为多个 Dataset
, 传入 randomSplit
的数组有多少个权重, 最终就会生成多少个 Dataset
, 这些权重的加倍和应该为 1, 否则将被标准化
val ds = spark.range(15)
val datasets: Array[Dataset[lang.Long]] = ds.randomSplit(Array[Double](2, 3))
datasets.foreach(dataset => dataset.show())
sample
sample
会随机在 Dataset
中抽样
val ds = spark.range(15)
ds.sample(withReplacement = false, fraction = 0.4).show()
排序
orderBy
orderBy
配合 Column
的 API
, 可以实现正反序排列
import spark.implicits._
val ds = Seq(Person("zhangsan", 12), Person("zhangsan", 8), Person("lisi", 15)).toDS()
ds.orderBy("age").show()
ds.orderBy('age.desc).show()
sort
其实 orderBy
是 sort
的别名, 所以它们所实现的功能是一样的
import spark.implicits._
val ds = Seq(Person("zhangsan", 12), Person("zhangsan", 8), Person("lisi", 15)).toDS()
ds.sort('age.desc).show()
分区
coalesce
减少分区, 此算子和 RDD
中的 coalesce
不同, Dataset
中的 coalesce
只能减少分区数, coalesce
会直接创建一个逻辑操作, 并且设置 Shuffle
为 false
val ds = spark.range(15)
ds.coalesce(1).explain(true)
repartitions
repartitions
有两个作用, 一个是重分区到特定的分区数, 另一个是按照某一列来分区, 类似于 SQL
中的 DISTRIBUTE BY
val ds = Seq(Person("zhangsan", 12), Person("zhangsan", 8), Person("lisi", 15)).toDS()
ds.repartition(4)
ds.repartition('name)
去重
dropDuplicates
使用 dropDuplicates
可以去掉某一些列中重复的行
import spark.implicits._
val ds = spark.createDataset(Seq(Person("zhangsan", 15), Person("zhangsan", 15), Person("lisi", 15)))
ds.dropDuplicates("age").show()
distinct
当 dropDuplicates
中没有传入列名的时候, 其含义是根据所有列去重, dropDuplicates()
方法还有一个别名, 叫做 distinct
所以, 使用 distinct
也可以去重, 并且只能根据所有的列来去重
import spark.implicits._
val ds = spark.createDataset(Seq(Person("zhangsan", 15), Person("zhangsan", 15), Person("lisi", 15)))
ds.distinct().show()
集合操作
except
except
和 SQL
语句中的 except
一个意思, 是求得 ds1
中不存在于 ds2
中的数据, 其实就是差集
val ds1 = spark.range(1, 10)
val ds2 = spark.range(5, 15)
ds1.except(ds2).show()
intersect
求得两个集合的交集
val ds1 = spark.range(1, 10)
val ds2 = spark.range(5, 15)
ds1.intersect(ds2).show()
union
求得两个集合的并集
val ds1 = spark.range(1, 10)
val ds2 = spark.range(5, 15)
ds1.union(ds2).show()
limit
限制结果集数量
val ds = spark.range(1, 10)
ds.limit(3).show()
8.2. 无类型转换
分类 | 算子 | 解释 |
---|---|---|
选择 |
|
|
|
在
| |
|
通过
| |
|
修改列名
| |
剪除 | drop |
剪掉某个列
|
聚合 | groupBy |
按照给定的行进行分组
|
8.5. Column 对象
Column 表示了 Dataset 中的一个列, 并且可以持有一个表达式, 这个表达式作用于每一条数据, 对每条数据都生成一个值, 之所以有单独这样的一个章节是因为列的操作属于细节, 但是又比较常见, 会在很多算子中配合出现
分类 | 操作 | 解释 |
---|---|---|
创建 |
|
单引号
|
val c1: Symbol = 'name
$
同理, $
符号也是一个隐式转换, 同样通过 spark.implicits
导入, 通过 $
可以生成一个 Column
对象
val spark = SparkSession.builder().appName("column").master("local[6]").getOrCreate()
import spark.implicits._
val personDF = Seq(Person("zhangsan", 12), Person("zhangsan", 8), Person("lisi", 15)).toDS()
val c2: ColumnName = $“name”
col
SparkSQL
提供了一系列的函数, 可以通过函数实现很多功能, 在后面课程中会进行详细介绍, 这些函数中有两个可以帮助我们创建 Column
对象, 一个是 col
, 另外一个是 column
val spark = SparkSession.builder().appName("column").master("local[6]").getOrCreate()
import org.apache.spark.sql.functions._
val personDF = Seq(Person("zhangsan", 12), Person("zhangsan", 8), Person("lisi", 15)).toDS()
val c3: sql.Column = col(“name”)
column
val spark = SparkSession.builder().appName("column").master("local[6]").getOrCreate()
import org.apache.spark.sql.functions._
val personDF = Seq(Person("zhangsan", 12), Person("zhangsan", 8), Person("lisi", 15)).toDS()
val c4: sql.Column = column(“name”)
Dataset.col
前面的 Column
对象创建方式所创建的 Column
对象都是 Free
的, 也就是没有绑定任何 Dataset
, 所以可以作用于任何 Dataset
, 同时, 也可以通过 Dataset
的 col
方法选择一个列, 但是这个 Column
是绑定了这个 Dataset
的, 所以只能用于创建其的 Dataset
上
val spark = SparkSession.builder().appName("column").master("local[6]").getOrCreate()
val personDF = Seq(Person("zhangsan", 12), Person("zhangsan", 8), Person("lisi", 15)).toDS()
val c5: sql.Column = personDF.col(“name”)
Dataset.apply
可以通过 Dataset
对象的 apply
方法来获取一个关联此 Dataset
的 Column
对象
val spark = SparkSession.builder().appName("column").master("local[6]").getOrCreate()
val personDF = Seq(Person("zhangsan", 12), Person("zhangsan", 8), Person("lisi", 15)).toDS()
val c6: sql.Column = personDF.apply(“name”)
apply
的调用有一个简写形式
val c7: sql.Column = personDF("name")
别名和转换
as[Type]
as
方法有两个用法, 通过 as[Type]
的形式可以将一个列中数据的类型转为 Type
类型
personDF.select(col("age").as[Long]).show()
as(name)
通过 as(name)
的形式使用 as
方法可以为列创建别名
personDF.select(col("age").as("age_new")).show()
添加列
withColumn
通过 Column
在添加一个新的列时候修改 Column
所代表的列的数据
personDF.withColumn("double_age", 'age * 2).show()
操作
like
通过 Column
的 API
, 可以轻松实现 SQL
语句中 LIKE
的功能
personDF.filter('name like "%zhang%").show()
isin
通过 Column
的 API
, 可以轻松实现 SQL
语句中 ISIN
的功能
personDF.filter('name isin ("hello", "zhangsan")).show()
sort
在排序的时候, 可以通过 Column
的 API
实现正反序
personDF.sort('age.asc).show()
personDF.sort('age.desc).show()
9. 缺失值处理
-
DataFrame
中什么时候会有无效值 -
DataFrame
如何处理无效的值 -
DataFrame
如何处理null
-
缺失值的处理思路
- 如何使用
DataFrameNaFunctions
SparkSQL
处理
null
和
NaN
?
val df = spark.read
.option(“header”, value = true)
.schema(schema)
.csv(“dataset/beijingpm_with_nan.csv”)
对于缺失值的处理一般就是丢弃和填充
-
丢弃包含
-
当某行数据所有值都是
null
或者NaN
的时候丢弃此行df.na.drop("all").show()
当某行中特定列所有值都是
null
或者NaN
的时候丢弃此行df.na.drop("all", List("pm", "id")).show()
当某行数据任意一个字段为
null
或者NaN
的时候丢弃此行df.na.drop().show() df.na.drop("any").show()
当某行中特定列任意一个字段为
null
或者NaN
的时候丢弃此行df.na.drop(List("pm", "id")).show() df.na.drop("any", List("pm", "id")).show()
填充包含
-
填充所有包含
null
和NaN
的列df.na.fill(0).show()
填充特定包含
null
和NaN
的列df.na.fill(0, List("pm")).show()
根据包含
null
和NaN
的列的不同来填充import scala.collection.JavaConverters._
null
和
NaN
的行
null
和
NaN
的列
df.na.fill(Map[String, Any](“pm” -> 0).asJava).show
如何使用SparkSQL
处理异常字符串 ?
10. 聚合
-
groupBy
-
rollup
-
cube
-
pivot
-
RelationalGroupedDataset
上的聚合操作
groupBy
import spark.implicits._
private val schema = StructType(
List(
StructField(“id”, IntegerType),
StructField(“year”, IntegerType),
StructField(“month”, IntegerType),
StructField(“day”, IntegerType),
StructField(“hour”, IntegerType),
StructField(“season”, IntegerType),
StructField(“pm”, DoubleType)
)
)
private val pmDF = spark.read
.schema(schema)
.option(“header”, value = true)
.csv(“dataset/pm_without_null.csv”)
functions
函数进行聚合
import org.apache.spark.sql.functions._
val groupedDF: RelationalGroupedDataset = pmDF.groupBy('year)
groupedDF.agg(avg('pm) as “pm_avg”)
.orderBy('pm_avg)
.show()
functions
进行聚合, 还可以直接使用
RelationalGroupedDataset
的
API
进行聚合
groupedDF.avg("pm")
.orderBy('pm_avg)
.show()
groupedDF.max(“pm”)
.orderBy('pm_avg)
.show()
import spark.implicits._
private val schemaFinal = StructType(
List(
StructField(“source”, StringType),
StructField(“year”, IntegerType),
StructField(“month”, IntegerType),
StructField(“day”, IntegerType),
StructField(“hour”, IntegerType),
StructField(“season”, IntegerType),
StructField(“pm”, DoubleType)
)
)
private val pmFinal = spark.read
.schema(schemaFinal)
.option(“header”, value = true)
.csv(“dataset/pm_final.csv”)
import org.apache.spark.sql.functions._
val groupPostAndYear = pmFinal.groupBy('source, 'year)
.agg(sum(“pm”) as “pm”)
val groupPost = pmFinal.groupBy('source)
.agg(sum(“pm”) as “pm”)
.select('source, lit(null) as “year”, 'pm)
groupPostAndYear.union(groupPost)
.sort('source, 'year asc_nulls_last, 'pm)
.show()
大家其实也能看出来, 在一个数据集中又小计又总计, 可能需要多个操作符, 如何简化呢? 请看下面
rollup
操作符
val sales = Seq(
(“Beijing”, 2016, 100),
(“Beijing”, 2017, 200),
(“Shanghai”, 2015, 50),
(“Shanghai”, 2016, 150),
(“Guangzhou”, 2017, 50)
).toDF(“city”, “year”, “amount”)
rollup
的操作
sales.rollup("city", "year")
.agg(sum("amount") as "amount")
.sort($"city".desc_nulls_last, $"year".asc_nulls_last)
.show()
/**
- 结果集:
- ±--------±—±-----+
- | city|year|amount|
- ±--------±—±-----+
- | Shanghai|2015| 50| <-- 上海 2015 的小计
- | Shanghai|2016| 150|
- | Shanghai|null| 200| <-- 上海的总计
- |Guangzhou|2017| 50|
- |Guangzhou|null| 50|
- | Beijing|2016| 100|
- | Beijing|2017| 200|
- | Beijing|null| 300|
- | null|null| 550| <-- 整个数据集的总计
- ±--------±—±-----+
*/
val cityAndYear = sales
.groupBy("city", "year") // 按照 city 和 year 聚合
.agg(sum("amount") as "amount")
val city = sales
.groupBy(“city”) // 按照 city 进行聚合
.agg(sum(“amount”) as “amount”)
.select($“city”, lit(null) as “year”, $“amount”)
val all = sales
.groupBy() // 全局聚合
.agg(sum(“amount”) as “amount”)
.select(lit(null) as “city”, lit(null) as “year”, $“amount”)
cityAndYear
.union(city)
.union(all)
.sort($“city”.desc_nulls_last, $“year”.asc_nulls_last)
.show()
/**
- 统计结果:
- ±--------±—±-----+
- | city|year|amount|
- ±--------±—±-----+
- | Shanghai|2015| 50|
- | Shanghai|2016| 150|
- | Shanghai|null| 200|
- |Guangzhou|2017| 50|
- |Guangzhou|null| 50|
- | Beijing|2016| 100|
- | Beijing|2017| 200|
- | Beijing|null| 300|
- | null|null| 550|
- ±--------±—±-----+
*/
很明显可以看到, 在上述案例中, rollup
就相当于先按照 city
, year
进行聚合, 后按照 city
进行聚合, 最后对整个数据集进行聚合, 在按照 city
聚合时, year
列值为 null
, 聚合整个数据集的时候, 除了聚合列, 其它列值都为 null
rollup
完成
pm
值的统计
pmFinal.rollup('source, 'year)
.agg(sum(“pm”) as “pm_total”)
.sort('source.asc_nulls_last, 'year.asc_nulls_last)
.show()
cube
pmFinal.cube('source, 'year)
.agg(sum(“pm”) as “pm_total”)
.sort('source.asc_nulls_last, 'year.asc_nulls_last)
.show()
/**
- 结果集为
- ±------±—±--------+
- | source|year| pm_total|
- ±------±—±--------+
- | dongsi|2013| 735606.0|
- | dongsi|2014| 745808.0|
- | dongsi|2015| 752083.0|
- | dongsi|null|2233497.0|
- |us_post|2010| 841834.0|
- |us_post|2011| 796016.0|
- |us_post|2012| 750838.0|
- |us_post|2013| 882649.0|
- |us_post|2014| 846475.0|
- |us_post|2015| 714515.0|
- |us_post|null|4832327.0|
- | null|2010| 841834.0| <-- 新增
- | null|2011| 796016.0| <-- 新增
- | null|2012| 750838.0| <-- 新增
- | null|2013|1618255.0| <-- 新增
- | null|2014|1592283.0| <-- 新增
- | null|2015|1466598.0| <-- 新增
- | null|null|7065824.0|
- ±------±—±--------+
*/
SparkSQL
中支持的
SQL
语句实现
cube
功能
RelationalGroupedDataset
11. 连接
-
无类型连接
join
-
连接类型
Join Types
-
无类型连接算子
join
的
API
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|
- ±–±-----±--------+
*/
现在两个表连接得到了如下的表
+---+------+---------+
| id| name| city|
+---+------+---------+
| 0| Lucy| Beijing|
| 1| Lily| Beijing|
| 2| Tim|Guangzhou|
| 3|Danial| Beijing|
+---+------+---------+
通过对这张表的查询, 这个查询是作用于两张表的, 所以是同一时间访问了多条数据
spark.sql("select name from user_city where city = 'Beijing'").show()
/**
- 执行结果
- ±-----+
- | name|
- ±-----+
- | Lucy|
- | Lily|
- |Danial|
- ±-----+
*/
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”)
连接类型 | 类型字段 | 解释 |
---|---|---|
交叉连接 |
|
SQL 语句
Dataset 操作
|
内连接 |
|
SQL 语句
Dataset 操作
|
全外连接 |
|
SQL 语句
Dataset 操作
|
左外连接 |
|
SQL 语句
Dataset 操作
|
|
|
SQL 语句
Dataset 操作
|
|
|
SQL 语句
Dataset 操作
|
右外连接 |
|
SQL 语句
Dataset 操作
|
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)
在使用 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 也可以使用特殊的语法开启
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", -1)
val resultDF = spark.sql(
"""
|select /*+ MAPJOIN (rt) */ * from person cross join cities rt
""".stripMargin)
println(resultDF.queryExecution.sparkPlan.numberedTreeString)
12. 窗口函数
-
目标和步骤
12.1. 第一名和第二名案例
-
目标和步骤
- 需求介绍
- 代码编写
@Test
def firstSecond(): Unit = {
val spark = SparkSession.builder()
.appName(“window”)
.master(“local[6]”)
.getOrCreate()
import spark.implicits._
val data = Seq(
("Thin", "Cell phone", 6000),
("Normal", "Tablet", 1500),
("Mini", "Tablet", 5500),
("Ultra thin", "Cell phone", 5000),
("Very thin", "Cell phone", 6000),
("Big", "Tablet", 2500),
("Bendable", "Cell phone", 3000),
("Foldable", "Cell phone", 3000),
("Pro", "Tablet", 4500),
("Pro2", "Tablet", 6500)
)
val source = data.toDF("product", "category", "revenue")
}
}
-
方式一:
SQL
语句::SELECT product, category, revenue FROM ( SELECT product, category, revenue, dense_rank() OVER (PARTITION BY category ORDER BY revenue DESC) as rank FROM productRevenue) tmp WHERE rank <= 2
-
窗口函数在
SQL
中的完整语法如下function OVER (PARITION BY ... ORDER BY ... FRAME_TYPE BETWEEN ... AND ...)
-
-
方式二: 使用
DataFrame
的命令式API
::val window: WindowSpec = Window.partitionBy('category) .orderBy('revenue.desc)
source.select('product, 'category, 'revenue, dense_rank() over window as “rank”)
.where('rank <= 2)
.show()-
WindowSpec
: 窗口的描述符, 描述窗口应该是怎么样的 -
dense_rank() over window
: 表示一个叫做dense_rank()
的函数作用于每一个窗口
12.2. 窗口函数
-
目标和步骤
- 窗口函数的逻辑
- 窗口定义部分
- 函数部分
- 总结
12.3. 最优差值案例
-
目标和步骤
- 需求介绍
- 代码实现
import spark.implicits._
import org.apache.spark.sql.functions._val data = Seq(
(“Thin”, “Cell phone”, 6000),
(“Normal”, “Tablet”, 1500),
(“Mini”, “Tablet”, 5500),
(“Ultra thin”, “Cell phone”, 5500),
(“Very thin”, “Cell phone”, 6000),
(“Big”, “Tablet”, 2500),
(“Bendable”, “Cell phone”, 3000),
(“Foldable”, “Cell phone”, 3000),
(“Pro”, “Tablet”, 4500),
(“Pro2”, “Tablet”, 6500)
)val source = data.toDF(“product”, “category”, “revenue”)
val windowSpec = Window.partitionBy('category)
.orderBy('revenue.desc)source.select(
'product, 'category, 'revenue,
((max('revenue) over windowSpec) - 'revenue) as 'revenue_difference
).show()-