SparkSQL
SparkSQL
数据分析的方式
数据分析的方式大致上可以划分为 SQL
和 命令式两种
命令式
在前面的
RDD
部分, 非常明显可以感觉的到是命令式的, 主要特征是通过一个算子, 可以得到一个结果, 通过结果再进行后续计算sc.textFile("...")
.flatMap(_.split(" "))
.map((_, 1))
.reduceByKey(_ + _)
.collect()
命令式的优点
命令式的优点操作粒度更细, 能够控制数据的每一个处理环节操作更明确, 步骤更清晰, 容易维护支持非结构化数据的操作
命令式的缺点
需要一定的代码功底
写起来比较麻烦
SQL
对于一些数据科学家, 要求他们为了做一个非常简单的查询, 写一大堆代码, 明显是一件非常残忍的事情, 所以
SQL on Hadoop
是一个非常重要的方向.SELECT name, age, school FROM students WHERE age > 10
SQL 的优点
表达非常清晰, 比如说这段
SQL
明显就是为了查询三个字段, 又比如说这段SQL
明显能看到是想查询年龄大于 10 岁的条目SQL 的缺点
想想一下 3 层嵌套的
SQL
, 维护起来应该挺力不从心的吧试想一下, 如果使用
SQL
来实现机器学习算法, 也挺为难的吧
SQL
擅长数据分析和通过简单的语法表示查询, 命令式操作适合过程式处理和算法性的处理. 在 Spark
出现之前, 对于结构化数据的查询和处理, 一个工具一向只能支持 SQL
或者命令式, 使用者被迫要使用多个工具来适应两种场景, 并且多个工具配合起来比较费劲.
而 Spark
出现了以后, 统一了两种数据处理范式, 是一种革新性的进步.
SparkSQL 的适用场景
定义 | 特点 | 特点举例 |
---|---|---|
结构化数据 | 有固定的 Schema | 有预定义的 Schema |
半结构化数据 | 没有固定的 Schema , 但是有结构 | 没有固定的 Schema , 有结构信息, 数据一般是自描述的 |
非结构化数据 | 没有固定 Schema , 也没有结构 | 没有固定 Schema , 也没有结构 |
结构化数据
一般指数据有固定的 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"
}
]
}
SparkSQL
处理什么数据的问题?
Spark
的RDD
主要用于处理 非结构化数据 和 半结构化数据SparkSQL
主要用于处理 结构化数据
SparkSQL
相较于 RDD
的优势在哪?
SparkSQL
提供了更好的外部数据源读写支持- 因为大部分外部数据源是有结构化的, 需要在
RDD
之外有一个新的解决方案, 来整合这些结构化数据源
- 因为大部分外部数据源是有结构化的, 需要在
SparkSQL
提供了直接访问列的能力- 因为
SparkSQL
主要用做于处理结构化数据, 所以其提供的API
具有一些普通数据库的能力
- 因为
SparkSQL 初体验
SparkSession
SparkContext
作为RDD
的创建者和入口, 其主要作用有如下两点创建
RDD
, 主要是通过读取文件创建RDD
监控和调度任务, 包含了一系列组件, 例如DAGScheduler
,TaskSheduler
为什么无法使用
SparkContext
作为SparkSQL
的入口?SparkContext
在读取文件的时候, 是不包含Schema
信息的, 因为读取出来的是RDD``SparkContext
在整合数据源如Cassandra
,JSON
,Parquet
等的时候是不灵活的, 而DataFrame
和Dataset
一开始的设计目标就是要支持更多的数据源SparkContext
的调度方式是直接调度RDD
, 但是一般情况下针对结构化数据的访问, 会先通过优化器优化一下
所以 SparkContext
确实已经不适合作为 SparkSQL
的入口, 所以刚开始的时候 Spark
团队为 SparkSQL
设计了两个入口点, 一个是 SQLContext
对应 Spark
标准的 SQL
执行, 另外一个是 HiveContext
对应 HiveSQL
的执行和 Hive
的支持.
在 Spark 2.0
的时候, 为了解决入口点不统一的问题, 创建了一个新的入口点 SparkSession
, 作为整个 Spark
生态工具的统一入口点, 包括了 SQLContext
, HiveContext
, SparkContext
等组件的功能
新的入口应该有什么特性?
能够整合
SQLContext
,HiveContext
,SparkContext
,StreamingContext
等不同的入口点为了支持更多的数据源, 应该完善读取和写入体系同时对于原来的入口点也不能放弃, 要向下兼容
命令式 API与SQL 的入门案例
SparkSQL 中有一个新的类型叫做 Dataset
case class People(name: String, age: Int)
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()
val teenagers: Dataset[String] = peopleDS.where('age > 10)
.where('age < 20)
.select('name)
.as[String]
/*
+----+
|name|
+----+
|lisi|
+----+
*/
teenagers.show()
////DF
@Test
def test2(): Unit ={
val spark: SparkSession = new SparkSession.Builder().appName("test2").master("local[6]").getOrCreate()
import spark.implicits._
val sourceRDD: RDD[People] = spark.sparkContext.parallelize(Seq(People("zhangsan", 10), People("lisi", 15)))
val df: DataFrame = sourceRDD.toDF()
//创建临时表并赋予名字
df.createOrReplaceTempView("person")
val result: DataFrame = spark.sql("select name from person where age > 10 and age <20")
result.show()
}
/*
+----+
|name|
+----+
|lisi|
+----+
*以往使用 SQL 肯定是要有一个表的, 在 Spark 中, 并不存在表的概念, 但是有一个近似的概念, 叫做 DataFrame, 所以一般情况下要先通过 DataFrame 或者 Dataset 注册一张临时表, 然后使用 SQL 操作这张临时表
*/
DataFrame & Dataset
SparkSQL 最大的特点就是它针对于结构化数据设计, 所以 SparkSQL
应该是能支持针对某一个字段的访问的, 而这种访问方式有一个前提, 就是 SparkSQL
的数据集中, 要 包含结构化信息, 也就是俗称的 Schema
而 SparkSQL
对外提供的 API
有两类, 一类是直接执行 SQL
, 另外一类就是命令式. SparkSQL
提供的命令式 API
就是 DataFrame
和 Dataset
, 暂时也可以认为 DataFrame
就是 Dataset
, 只是在不同的 API
中返回的是 Dataset
的不同表现形式
Catalyst
SparkSQL
和 RDD
不同的主要点是在于其所操作的数据是结构化的, 提供了对数据更强的感知和分析能力, 能够对代码进行更深层的优化, 而这种能力是由一个叫做 Catalyst
的优化器所提供的
Catalyst
的主要运作原理是分为三步, 先对 SQL
或者 Dataset
的代码解析, 生成逻辑计划, 后对逻辑计划进行优化, 再生成物理计划, 最后生成代码到集群中以 RDD
的形式运行
Dataset 的特点
Dataset API例子
filter方法
val spark: SparkSession = new sql.SparkSession.Builder()
.appName("hello")
.master("local[6]")
.getOrCreate()
import spark.implicits._
//创建dataset
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()
Dataset
是一个强类型, 并且类型安全的数据容器, 并且提供了结构化查询 API
和类似 RDD
一样的命令式 API
Dataset
的底层是什么?
Dataset
最底层处理的是对象的序列化形式, 通过查看 Dataset
生成的物理执行计划, 也就是最终所处理的 RDD
, 就可以判定 Dataset
底层处理的是什么形式的数据
val dataset: Dataset[People] = spark.createDataset(Seq(People("zhangsan", 9), People("lisi", 15)))
val internalRDD: RDD[InternalRow] = dataset.queryExecution.toRdd
dataset.queryExecution.toRdd` 这个 `API` 可以看到 `Dataset` 底层执行的 `RDD`, 这个 `RDD` 中的范型是 `InternalRow`, `InternalRow` 又称之为 `Catalyst Row`, 是 `Dataset` 底层的数据结构, 也就是说, 无论 `Dataset` 的范型是什么, 无论是 `Dataset[Person]` 还是其它的, 其最底层进行处理的数据结构都是 `InternalRow
所以, Dataset
的范型对象在执行之前, 需要通过 Encoder
转换为 InternalRow
, 在输入之前, 需要把 InternalRow
通过 Decoder
转换为范型对象
总结
Dataset
是一个新的Spark
组件, 其底层还是RDD
Dataset
提供了访问对象中某个特定字段的能力, 不用像RDD
一样每次都要针对整个对象做操作Dataset
和RDD
不同, 如果想把Dataset[T]
转为RDD[T]
, 则需要对Dataset
底层的InternalRow
做转换, 是一个比价重量级的操作
DataFrame 的作用和常见操作
DataFrame
是什么?
DataFrame
是 SparkSQL
中一个表示关系型数据库中 表
的函数式抽象, 其作用是让 Spark
处理大规模结构化数据的时候更加容易. 一般 DataFrame
可以处理结构化的数据, 或者是半结构化的数据, 因为这两类数据中都可以获取到 Schema
信息. 也就是说 DataFrame
中有 Schema
信息, 可以像操作表一样操作 DataFrame
.
DataFrame
由两部分构成, 一是 row
的集合, 每个 row
对象表示一个行, 二是描述 DataFrame
结构的 Schema
.
DataFrame
支持 SQL
中常见的操作, 例如: select
, filter
, join
, group
, sort
, join
等
测试
@Test
def test5(): Unit ={
val spark: SparkSession = SparkSession.builder().appName("test5").master("local[6]").getOrCreate()
import spark.implicits._
val people: DataFrame = Seq(People("zhangshan",19),People("lsi",9),People("zhaoliu",19)).toDF()
people.groupBy('age)
.count()
.show()
//+---+-----+
//|age|count|
//+---+-----+
//| 19| 2|
//| 9| 1|
//+---+-----+
}
其他方式:
通过隐式转换创建 DataFrame
根据源码可以知道, 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 spark: SparkSession = new sql.SparkSession.Builder()
.appName("hello")
.master("local[6]")
.getOrCreate()
val df = spark.read
.option("header", true)
.csv("dataset/BeijingPM20100101_20151231.csv")
df.show(10)
df.printSchema()
其他操作
需求: 查看每个月的统计数量
- Step 1: 首先可以打印
DataFrame
的Schema
, 查看其中所包含的列, 以及列的类型 - Step 2: 对于大部分计算来说, 可能不会使用所有的列, 所以可以选择其中某些重要的列
- Step 3: 可以针对某些列进行分组, 后对每组数据通过函数做聚合
@Test
def test8(): Unit ={
val spark: SparkSession = SparkSession.builder().master("local[6]").appName("tets8").getOrCreate()
import spark.implicits._
val df: DataFrame = spark.read
//参数1;"header"指定第一行为表结构
.option("header", value = true)
.csv("dataset/BeijingPM20100101_20151231.csv")
df.select('year, 'month, 'PM_Dongsi)
.where('PM_Dongsi =!= "Na")
.groupBy('year, 'month)
.count()
.show()
}
- 使用
SQL
操作DataFrame
使用 SQL
来操作某个 DataFrame
的话, SQL
中必须要有一个 from
子句, 所以需要先将 DataFrame
注册为一张临时表
@Test
def test9(): Unit ={
val spark: SparkSession = SparkSession.builder().appName("hello")
.master("local[6]")
.getOrCreate()
val df: DataFrame = 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 order by year").show()
}
Dataset 和 DataFrame 的异同
DataFrame
就是 Dataset
DataFrame
和 Dataset
所表达的语义不同
第一点: DataFrame 表达的含义是一个支持函数式操作的 表, 而 Dataset 表达是是一个类似 RDD 的东西, Dataset 可以处理任何对象
第二点: DataFrame
中所存放的是 Row
对象, 而 Dataset
中可以存放任何类型的对象
第三点: DataFrame
的操作方式和 Dataset
是一样的, 但是对于强类型操作而言, 它们处理的类型不同
第四点: DataFrame
只能做到运行时类型检查, Dataset
能做到编译和运行时都有类型检查
Row
对象表示的是一个行
Row
的操作类似于Scala
中的Map
数据类型
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
, 是一个行的概念
数据读写
初识 DataFrameReader
SparkSQL
的一个非常重要的目标就是完善数据读取, 所以 SparkSQL
中增加了一个新的框架, 专门用于读取外部数据源, 叫做 DataFrameReader
DataFrameReader
由如下几个组件组成
组件 | 解释 |
---|---|
schema | 结构信息, 因为 Dataset 是有结构的, 所以在读取数据的时候, 就需要有 Schema 信息, 有可能是从外部数据源获取的, 也有可能是指定的 |
option | 连接外部数据源的参数, 例如 JDBC 的 URL , 或者读取 CSV 文件是否引入 Header 等 |
format | 外部数据源的格式, 例如 csv , jdbc , json 等 |
DataFrameReader
有两种访问方式, 一种是使用 load
方法加载, 使用 format
指定加载格式, 还有一种是使用封装方法, 类似 csv
, json
, jdbc
等
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
的封装
初识 DataFrameWriter
对于 ETL
来说, 数据保存和数据读取一样重要, 所以 SparkSQL
中增加了一个新的数据写入框架, 叫做 DataFrameWriter
DataFrameWriter
中由如下几个部分组成
组件 | 解释 |
---|---|
source | 写入目标, 文件格式等, 通过 format 方法设定 |
mode | 写入模式, 例如一张表已经存在, 如果通过 DataFrameWriter 向这张表中写入数据, 是覆盖表呢, 还是向表中追加呢? 通过 mode 方法设定 |
extraOptions | 外部参数, 例如 JDBC 的 URL , 通过 options , option 设定 |
partitioningColumns | 类似 Hive 的分区, 保存表的时候使用, 这个地方的分区不是 RDD 的分区, 而是文件的分区, 或者表的分区, 通过 partitionBy 设定 |
bucketColumnNames | 类似 Hive 的分桶, 保存表的时候使用, 通过 bucketBy 设定 |
sortColumnNames | 用于排序的列, 通过 sortBy 设定 |
mode
指定了写入模式, 例如覆盖原数据集, 或者向原数据集合中尾部添加等
Scala 对象表示 | 字符串表示 | 解释 |
---|---|---|
SaveMode.ErrorIfExists | "error" | 将 DataFrame 保存到 source 时, 如果目标已经存在, 则报错 |
SaveMode.Append | "append" | 将 DataFrame 保存到 source 时, 如果目标已经存在, 则添加到文件或者 Table 中 |
SaveMode.Overwrite | "overwrite" | 将 DataFrame 保存到 source 时, 如果目标已经存在, 则使用 DataFrame 中的数据完全覆盖目标 |
SaveMode.Ignore | "ignore" | 将 DataFrame 保存到 source 时, 如果目标已经存在, 则不会保存 DataFrame 数据, 并且也不修改目标数据集, 类似于 CREATE TABLE IF NOT EXISTS |
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")
默认没有指定
format
, 默认的format
是Parquet
总结
- 类似
DataFrameReader
,Writer
中也有format
,options
, 另外schema
是包含在DataFrame
中的 DataFrameWriter
中还有一个很重要的概念叫做mode
, 指定写入模式, 如果目标集合已经存在时的行为DataFrameWriter
可以将数据保存到Hive
表中, 所以也可以指定分区和分桶信息
读写 Parquet 格式文件
在ETL中,spark将文件读写到HDFS可能会使用到Parquet
为了能够保存比较复杂的数据, 并且保证性能和压缩率, 通常使用 Parquet
是一个比较不错的选择.
//默认不指定 format 的时候, 默认就是读写 Parquet 格式的文件
val spark: SparkSession = new sql.SparkSession.Builder()
.appName("hello")
.master("local[6]")
.getOrCreate()
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
的时候可以指定分区
val spark: SparkSession = new sql.SparkSession.Builder()
.appName("hello")
.master("local[6]")
.getOrCreate()
// 从 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")
读写 JSON 格式文件
什么时候会用到 JSON
?
在 ETL
中, Spark
经常扮演 T
的职务, 也就是进行数据清洗和数据转换.
在业务系统中, JSON
是一个非常常见的数据格式, 在前后端交互的时候也往往会使用 JSON
, 所以从业务系统获取的数据很大可能性是使用 JSON
格式, 所以就需要 Spark
能够支持 JSON 格式文件的读取
读写 JSON
文件
val spark: SparkSession = new sql.SparkSession.Builder()
.appName("hello")
.master("local[6]")
.getOrCreate()
val dfFromParquet = spark.read.load("dataset/beijing_pm")
// 将 DataFrame 保存为 JSON 格式的文件
dfFromParquet.repartition(1)
.write.format("json")
.save("dataset/beijing_pm_json")
如果不重新分区, 则会为 DataFrame
底层的 RDD
的每个分区生成一个文件, 为了保持只有一个输出文件, 所以重新分区
保存为
JSON
格式的文件有一个细节需要注意, 这个JSON
格式的文件中, 每一行是一个独立的JSON
, 但是整个文件并不只是一个JSON
字符串, 所以这种文件格式很多时候被成为JSON Line
文件, 有时候后缀名也会变为jsonl
Spark
读取 JSON Line
文件的时候, 会自动的推断类型信息
Spark
可以从一个保存了 JSON
格式字符串的 Dataset[String]
中读取 JSON
信息, 转为 DataFrame
val spark: SparkSession = ...
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
, 并且会自动处理数据类型信息
SparkSQL 整合 Hive
整合什么 ?
MetaStore
, 元数据存储SparkSQL
内置的有一个MetaStore
, 通过嵌入式数据库Derby
保存元信息, 但是对于生产环境来说, 还是应该使用Hive
的MetaStore
, 一是更成熟, 功能更强, 二是可以使用Hive
的元信息查询引擎
SparkSQL
内置了HiveSQL
的支持, 所以无需整合
MetaStore
Hive
的 MetaStore
是一个 Hive
的组件, 一个 Hive
提供的程序, 用以保存和访问表的元数据, 整个 Hive
的结构大致如下
由上图可知道, 其实 Hive
中主要的组件就三个, HiveServer2
负责接受外部系统的查询请求, 例如 JDBC
, HiveServer2
接收到查询请求后, 交给 Driver
处理, Driver
会首先去询问 MetaStore
表在哪存, 后 Driver
程序通过 MR
程序来访问 HDFS
从而获取结果返回给查询请求者
而 Hive
的 MetaStore
对 SparkSQL
的意义非常重大, 如果 SparkSQL
可以直接访问 Hive
的 MetaStore
, 则理论上可以做到和 Hive
一样的事情, 例如通过 Hive
表查询数据
而 Hive 的 MetaStore 的运行模式有三种
内嵌
Derby
数据库模式这种模式不必说了, 自然是在测试的时候使用, 生产环境不太可能使用嵌入式数据库, 一是不稳定, 二是这个
Derby
是单连接的, 不支持并发Local
模式Local
和Remote
都是访问MySQL
数据库作为存储元数据的地方, 但是Local
模式的MetaStore
没有独立进程, 依附于HiveServer2
的进程Remote
模式和
Loca
模式一样, 访问MySQL
数据库存放元数据, 但是Remote
的MetaStore
运行在独立的进程中
我们显然要选择 Remote
模式, 因为要让其独立运行, 这样才能让 SparkSQL
一直可以访问
在 Hive
中创建表
第一步, 需要先将文件上传到集群中, 使用如下命令上传到 HDFS
中
hdfs dfs -mkdir -p /dataset
hdfs dfs -put studenttabl10k /dataset/
第二步, 使用 Hive
或者 Beeline
执行如下 SQL
CREATE DATABASE IF NOT EXISTS spark_integrition;
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
的表
查询 Hive
中的表可以直接通过 spark.sql(…)
来进行, 可以直接在其中访问 Hive
的 MetaStore
, 前提是一定要将 Hive
的配置文件拷贝到 Spark
的 conf
目录
scala> spark.sql("use spark_integrition")
scala> val resultDF = spark.sql("select * from student limit 10")
scala> resultDF.show()
通过 SparkSQL
创建 Hive
表
通过 SparkSQL
可以直接创建 Hive
表, 并且使用 LOAD DATA
加载数据
val createTableStr =
"""
|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'
""".stripMargin
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 表
前面都在使用 SparkShell
的方式来访问 Hive
, 编写 SQL
, 通过 Spark
独立应用的形式也可以做到同样的事, 但是需要一些前置的步骤, 如下
Step 1: 导入
Maven
依赖<dependency> <groupId>org.apache.spark</groupId> <artifactId>spark-hive_2.11</artifactId> <version>${spark.version}</version> </dependency>
Step 2: 配置
SparkSession
如果希望使用
SparkSQL
访问Hive
的话, 需要做两件事开启SparkSession
的Hive
支持经过这一步配置,SparkSQL
才会把SQL
语句当作HiveSQL
来进行解析设置WareHouse
的位置虽然hive-stie.xml
中已经配置了WareHouse
的位置, 但是在Spark 2.0.0
后已经废弃了hive-site.xml
中设置的hive.metastore.warehouse.dir
, 需要在SparkSession
中设置WareHouse
的位置设置MetaStore
的位置val spark = SparkSession .builder() .appName("hive example") .config("spark.sql.warehouse.dir", "hdfs://node01:8020/dataset/hive") .config("hive.metastore.uris", "thrift://node01:9083")
`.enableHiveSupport()
.getOrCreate()
设置
WareHouse的位置设置
MetaStore的位置开启
Hive` 支持
配置好了以后, 就可以通过 DataFrame
处理数据, 后将数据结果推入 Hive
表中了, 在将结果保存到 Hive
表的时候, 可以指定保存模式
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")
val resultDF = studentDF.where("age < 50")
resultDF.write.mode(SaveMode.Overwrite).saveAsTable("spark_integrition1.student")
通过 mode
指定保存模式, 通过 saveAsTable
保存数据到 Hive
JDBC
准备 MySQL
环境
在使用 SparkSQL
访问 MySQL
之前, 要对 MySQL
进行一些操作, 例如说创建用户, 表和库等
Step 1: 连接
MySQL
数据库在
MySQL
所在的主机上执行如下命令mysql -u root -p
Step 2: 创建
Spark
使用的用户登进
MySQL
后, 需要先创建用户
CREATE USER 'spark'@'%' IDENTIFIED BY 'Spark123!';
GRANT ALL ON spark_test.* TO 'spark'@'%';
Step 3: 创建库和表
CREATE DATABASE spark_test;
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
中写入数据
其实在使用 SparkSQL
访问 MySQL
是通过 JDBC
, 那么其实所有支持 JDBC
的数据库理论上都可以通过这种方式进行访问
在使用 JDBC
访问关系型数据的时候, 其实也是使用 DataFrameReader
, 对 DataFrameReader
提供一些配置, 就可以使用 Spark
访问 JDBC
, 有如下几个配置可用
属性 | 含义 |
---|---|
url | 要连接的 JDBC URL |
dbtable | 要访问的表, 可以使用任何 SQL 语句中 from 子句支持的语法 |
fetchsize | 数据抓取的大小(单位行), 适用于读的情况 |
batchsize | 数据传输的大小(单位行), 适用于写的情况 |
isolationLevel | 事务隔离级别, 是一个枚举, 取值 NONE , READ_COMMITTED , READ_UNCOMMITTED , REPEATABLE_READ , SERIALIZABLE , 默认为 READ_UNCOMMITTED |
读取数据集, 处理过后存往 MySQL
中的代码如下
val spark = SparkSession
.builder()
.appName("hive example")
.master("local[6]")
.getOrCreate()
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()
运行程序
如果是在本地运行, 需要导入 Maven
依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
如果使用 Spark submit
或者 Spark shell
来运行任务, 需要通过 --jars
参数提交 MySQL
的 Jar
包, 或者指定 --packages
从 Maven
库中读取
bin/spark-shell --packages mysql:mysql-connector-java:5.1.47 --repositories http://maven.aliyun.com/nexus/content/groups/public/
从 MySQL
中读取数据
读取 MySQL
的方式也非常的简单, 只是使用 SparkSQL
的 DataFrameReader
加上参数配置即可访问
spark.read.format("jdbc")
.option("url", "jdbc:mysql://node01:3306/spark_test")
.option("dbtable", "student")
.option("user", "spark")
.option("password", "Spark123!")
.load()
.show()
默认情况下读取 MySQL
表时, 从 MySQL
表中读取的数据放入了一个分区, 拉取后可以使用 DataFrame
重分区来保证并行计算和内存占用不会太高, 但是如果感觉 MySQL
中数据过多的时候, 读取时可能就会产生 OOM
, 所以在数据量比较大的场景, 就需要在读取的时候就将其分发到不同的 RDD
分区
属性 | 含义 |
---|---|
partitionColumn | 指定按照哪一列进行分区, 只能设置类型为数字的列, 一般指定为 ID |
lowerBound , upperBound | 确定步长的参数, lowerBound - upperBound 之间的数据均分给每一个分区, 小于 lowerBound 的数据分给第一个分区, 大于 upperBound 的数据分给最后一个分区 |
numPartitions | 分区数量 |
spark.read.format("jdbc")
.option("url", "jdbc:mysql://node01:3306/spark_test")
.option("dbtable", "student")
.option("user", "spark")
.option("password", "Spark123!")
.option("partitionColumn", "age")
.option("lowerBound", 1)
.option("upperBound", 60)
.option("numPartitions", 10)
.load()
.show()
有时候可能要使用非数字列来作为分区依据, Spark
也提供了针对任意类型的列作为分区依据的方法
val predicates = Array(
"age < 20",
"age >= 20, age < 30",
"age >= 30"
)
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()
Dataset (DataFrame) 的基础操作
有类型操作
分类 | 算子 | 解释 |
---|---|---|
转换 | flatMap | 通过 flatMap 可以将一条数据转为一个数组, 后再展开这个数组放入 Dataset``import spark.implicits._ val ds = Seq("hello world", "hello pc").toDS() ds.flatMap( _.split(" ") ).show() |
map | map 可以将数据集中每条数据转为另一种形式import spark.implicits._ val ds = Seq(Person("zhangsan", 15), Person("lisi", 15)).toDS() ds.map( person => Person(person.name, person.age * 2) ).show() | |
mapPartitions | mapPartitions 和 map 一样, 但是 map 的处理单位是每条数据, mapPartitions 的处理单位是每个分区import spark.implicits._ val ds = Seq(Person("zhangsan", 15), Person("lisi", 15)).toDS() ds.mapPartitions( iter => { val returnValue = iter.map( item => Person(item.name, item.age * 2) ) returnValue } ) .show() | |
transform | map 和 mapPartitions 以及 transform 都是转换, map 和 mapPartitions 是针对数据, 而 transform 是针对整个数据集, 这种方式最大的区别就是 transform 可以直接拿到 Dataset 进行操作import spark.implicits._ val ds = spark.range(5) ds.transform( dataset => dataset.withColumn("doubled", 'id * 2) ) | |
as | as[Type] 算子的主要作用是将弱类型的 Dataset 转为强类型的 Dataset , 它有很多适用场景, 但是最常见的还是在读取数据的时候, 因为 DataFrameReader 体系大部分情况下是将读出来的数据转换为 DataFrame 的形式, 如果后续需要使用 Dataset 的强类型 API , 则需要将 DataFrame 转为 Dataset . 可以使用 as[Type] 算子完成这种操作import spark.implicits._ 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() |
无类型转换
分类 | 算子 | 解释 |
---|---|---|
选择 | select | select 用来选择某些列出现在结果集中import spark.implicits._ val ds = Seq(Person("zhangsan", 12), Person("zhangsan", 8), Person("lisi", 15)).toDS() ds.select($"name").show() |
selectExpr | 在 SQL 语句中, 经常可以在 select 子句中使用 count(age) , rand() 等函数, 在 selectExpr 中就可以使用这样的 SQL 表达式, 同时使用 select 配合 expr 函数也可以做到类似的效果import spark.implicits._ import org.apache.spark.sql.functions._ val ds = Seq(Person("zhangsan", 12), Person("zhangsan", 8), Person("lisi", 15)).toDS() ds.selectExpr("count(age) as count").show() ds.selectExpr("rand() as random").show() ds.select(expr("count(age) as count")).show() | |
withColumn | 通过 Column 对象在 Dataset 中创建一个新的列或者修改原来的列import spark.implicits._ import org.apache.spark.sql.functions._ val ds = Seq(Person("zhangsan", 12), Person("zhangsan", 8), Person("lisi", 15)).toDS() ds.withColumn("random", expr("rand()")).show() | |
withColumnRenamed | 修改列名import spark.implicits._ val ds = Seq(Person("zhangsan", 12), Person("zhangsan", 8), Person("lisi", 15)).toDS() ds.withColumnRenamed("name", "new_name").show() | |
剪除 | drop | 剪掉某个列import spark.implicits._ val ds = Seq(Person("zhangsan", 12), Person("zhangsan", 8), Person("lisi", 15)).toDS() ds.drop('age).show() |
聚合 | groupBy | 按照给定的行进行分组import spark.implicits._ val ds = Seq(Person("zhangsan", 12), Person("zhangsan", 8), Person("lisi", 15)).toDS() ds.groupBy('name).count().show() |
Column 对象
分类 | 操作 | 解释 |
---|---|---|
创建 | ' | 单引号 ' 在 Scala 中是一个特殊的符号, 通过 ' 会生成一个 Symbol 对象, Symbol 对象可以理解为是一个字符串的变种, 但是比字符串的效率高很多, 在 Spark 中, 对 Scala 中的 Symbol 对象做了隐式转换, 转换为一个 ColumnName 对象, ColumnName 是 Column 的子类, 所以在 Spark 中可以如下去选中一个列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 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() |
缺失值处理
缺失值的处理思路
如果想探究如何处理无效值, 首先要知道无效值从哪来, 从而分析可能产生的无效值有哪些类型, 在分别去看如何处理无效值
什么是缺失值
一个值本身的含义是这个值不存在则称之为缺失值, 也就是说这个值本身代表着缺失, 或者这个值本身无意义, 比如说 null
, 比如说空字符串
关于数据的分析其实就是统计分析的概念, 如果这样的话, 当数据集中存在缺失值, 则无法进行统计和分析, 对很多操作都有影响
缺失值如何产生的
Spark 大多时候处理的数据来自于业务系统中, 业务系统中可能会因为各种原因, 产生一些异常的数据
例如说因为前后端的判断失误, 提交了一些非法参数. 再例如说因为业务系统修改 MySQL
表结构产生的一些空值数据等. 总之在业务系统中出现缺失值其实是非常常见的一件事, 所以大数据系统就一定要考虑这件事.
缺失值的类型
常见的缺失值有两种
null
,NaN
等特殊类型的值, 某些语言中null
可以理解是一个对象, 但是代表没有对象,NaN
是一个数字, 可以代表不是数字针对这一类的缺失值,
Spark
提供了一个名为DataFrameNaFunctions
特殊类型来操作和处理"Null"
,"NA"
," "
等解析为字符串的类型, 但是其实并不是常规字符串数据针对这类字符串, 需要对数据集进行采样, 观察异常数据, 总结经验, 各个击破
DataFrameNaFunctions
DataFrameNaFunctions
使用 Dataset
的 na
函数来获取
val df = ...
val naFunc: DataFrameNaFunctions = df.na
当数据集中出现缺失值的时候, 大致有两种处理方式, 一个是丢弃, 一个是替换为某值, DataFrameNaFunctions
中包含一系列针对空值数据的方案
DataFrameNaFunctions.drop
可以在当某行中包含null
或NaN
的时候丢弃此行DataFrameNaFunctions.fill
可以在将null
和NaN
充为其它值DataFrameNaFunctions.replace
可以把null
或NaN
替换为其它值, 但是和fill
略有一些不同, 这个方法针对值来进行替换
如何使用 SparkSQL
处理 null
和 NaN
?
首先要将数据读取出来, 此次使用的数据集直接存在 NaN
, 在指定 Schema
后, 可直接被转为 Double.NaN
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)
)
)
val df = spark.read
.option("header", value = true)
.schema(schema)
.csv("dataset/beijingpm_with_nan.csv")
对于缺失值的处理一般就是丢弃和填充
丢弃包含
null
和NaN
的行当某行数据所有值都是
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
的列填充所有包含
null
和NaN
的列df.na.fill(0).show()
填充特定包含null
和NaN
的列df.na.fill(0, List("pm")).show()
根据包含null
和NaN
的列的不同来填充import scala.collection.JavaConverters._ df.na.fill(Map[String, Any]("pm" -> 0).asJava).show
如何使用 SparkSQL
处理异常字符串 ?
读取数据集, 这次读取的是最原始的那个 PM
数据集
val df = spark.read
.option("header", value = true)
.csv("dataset/BeijingPM20100101_20151231.csv")
使用函数直接转换非法的字符串
df.select('No as "id", 'year, 'month, 'day, 'hour, 'season,
when('PM_Dongsi === "NA", 0)
.otherwise('PM_Dongsi cast DoubleType)
.as("pm"))
.show()
使用 where
直接过滤
df.select('No as "id", 'year, 'month, 'day, 'hour, 'season, 'PM_Dongsi)
.where('PM_Dongsi =!= "NA")
.show()
使用 DataFrameNaFunctions
替换, 但是这种方式被替换的值和新值必须是同类型
df.select('No as "id", 'year, 'month, 'day, 'hour, 'season, 'PM_Dongsi)
.na.replace("PM_Dongsi", Map("NA" -> "NaN"))
.show()
聚合
groupBy(单聚合)
groupBy
算子会按照列将 Dataset
分组, 并返回一个 RelationalGroupedDataset
对象, 通过 RelationalGroupedDataset
可以对分组进行聚合
加载实验数据
private val spark = SparkSession.builder()
.master("local[6]")
.appName("aggregation")
.getOrCreate()
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()
多维聚合
我们可能经常需要针对数据进行多维的聚合, 也就是一次性统计小计, 总计等, 一般的思路如下
准备数据
private val spark = SparkSession.builder()
.master("local[6]")
.appName("aggregation")
.getOrCreate()
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")
//结果集中添加一列可以使用lit ,
.select('source, lit(null) as "year", 'pm)
groupPostAndYear.union(groupPost)
.sort('source, 'year asc_nulls_last, 'pm)
.show()
rollup
操作符(在单聚合的基础上多一个总聚合)
rollup
操作符其实就是 groupBy
的一个扩展, rollup
会对传入的列进行滚动 groupBy
, groupBy
的次数为列数量 + 1
, 最后一次是对整个数据集进行聚合
创建数据集
import org.apache.spark.sql.functions._
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| <-- 整个数据集的总计
* +---------+----+------+
*/
import org.apache.spark.sql.functions._
sales.rollup("city","year")
.agg(sum('amount)as("amount_sum"))
.sort($"city".desc_nulls_last,$"year".asc_nulls_last)
.createOrReplaceTempView("test1")
spark.sql("select t.*,null test from test1 t").show()
/*
*
+---------+----+----------+----+
| city|year|amount_sum|test|
+---------+----+----------+----+
| Shanghai|2015| 50|null|
| Shanghai|2016| 150|null|
| Shanghai|null| 200|null|
|Guangzhou|2017| 50|null|
|Guangzhou|null| 50|null|
| Beijing|2016| 100|null|
| Beijing|2017| 200|null|
| Beijing|null| 300|null|
| null|null| 550|null|
+---------+----+----------+----+
*/
如果使用基础的 groupBy 如何实现效果?
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
cube
(在rollup的总聚合上再多出中间的其他聚合)
cube
的功能和 rollup
是一样的, 但也有区别, 区别如下
rollup(A, B).sum©
其结果集中会有三种数据形式:
A B C
,A null C
,null null C
不知道大家发现没, 结果集中没有对
B
列的聚合结果cube(A, B).sum©
其结果集中会有四种数据形式:
A B C
,A null C
,null null C
,null B C
不知道大家发现没, 比
rollup
的结果集中多了一个null B C
, 也就是说,rollup
只会按照第一个列来进行组合聚合, 但是cube
会将全部列组合聚合
import org.apache.spark.sql.functions._
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|
* +-------+----+---------+
*/
join连接
例子:
val person = Seq((0, "Lucy", 0), (1, "Lily", 0), (2, "Tim", 2), (3, "Danial", 0))
.toDF("id", "name", "cityId")
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|
* +---+------+---------+
*/
连接类型
准备
private val spark = SparkSession.builder()
.master("local[6]")
.appName("aggregation")
.getOrCreate()
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")
连接类型 | 类型字段 | 解释 |
---|---|---|
交叉连接 | cross | 解释交叉连接就是笛卡尔积, 就是两个表中所有的数据两两结对交叉连接是一个非常重的操作, 在生产中, 尽量不要将两个大数据集交叉连接, 如果一定要交叉连接, 也需要在交叉连接后进行过滤, 优化器会进行优化SQL 语句select * from person cross join cities``Dataset 操作person.crossJoin(cities) .where(person.col("cityId") === cities.col("id")) .show() |
内连接 | inner | 解释内连接就是按照条件找到两个数据集关联的数据, 并且在生成的结果集中只存在能关联到的数据SQL 语句select * from person inner join cities on person.cityId = cities.id``Dataset 操作person.join(right = cities, joinExprs = person("cityId") === cities("id"), joinType = "inner") .show() |
全外连接 | outer , full , fullouter | 解释内连接和外连接的最大区别, 就是内连接的结果集中只有可以连接上的数据, 而外连接可以包含没有连接上的数据, 根据情况的不同, 外连接又可以分为很多种, 比如所有的没连接上的数据都放入结果集, 就叫做全外连接SQL 语句select * from person full outer join cities on person.cityId = cities.id``Dataset 操作person.join(right = cities, joinExprs = person("cityId") === cities("id"), joinType = "full") // "outer", "full", "full_outer" .show() |
左外连接 | leftouter , left | 解释左外连接是全外连接的一个子集, 全外连接中包含左右两边数据集没有连接上的数据, 而左外连接只包含左边数据集中没有连接上的数据SQL 语句select * from person left join cities on person.cityId = cities.id``Dataset 操作person.join(right = cities, joinExprs = person("cityId") === cities("id"), joinType = "left") // leftouter, left .show() |
LeftAnti | leftanti | 解释LeftAnti 是一种特殊的连接形式, 和左外连接类似, 但是其结果集中没有右侧的数据, 只包含左边集合中没连接上的数据SQL 语句select * from person left anti join cities on person.cityId = cities.id``Dataset 操作person.join(right = cities, joinExprs = person("cityId") === cities("id"), joinType = "left_anti") .show() |
LeftSemi | leftsemi | 解释和 LeftAnti 恰好相反, LeftSemi 的结果集也没有右侧集合的数据, 但是只包含左侧集合中连接上的数据SQL 语句select * from person left semi join cities on person.cityId = cities.id``Dataset 操作person.join(right = cities, joinExprs = person("cityId") === cities("id"), joinType = "left_semi") .show() |
右外连接 | rightouter , right | 解释右外连接和左外连接刚好相反, 左外是包含左侧未连接的数据, 和两个数据集中连接上的数据, 而右外是包含右侧未连接的数据, 和两个数据集中连接上的数据SQL 语句select * from person right join cities on person.cityId = cities.id``Dataset 操作person.join(right = cities, joinExprs = person("cityId") === cities("id"), joinType = "right") // rightouter, right .show() |
UDF
//需求:将id不满8位,补足8位[补0]
test.json
{"id":"1","name":"a","clazz":1,"score":80}
{"id":"02","name":"b","clazz":1,"score":78}
{"id":"003","name":"c","clazz":1,"score":95}
{"id":"05","name":"d","clazz":2,"score":74}
{"id":"06","name":"e","clazz":2,"score":92}
{"id":"7","name":"f","clazz":3,"score":99}
{"id":"8","name":"g","clazz":3,"score":99}
{"id":"9","name":"h","clazz":3,"score":45}
{"id":"10","name":"i","clazz":3,"score":55}
{"id":"11","name":"j","clazz":3,"score":78}
UDFtest.scala
import org.apache.spark.sql.SparkSession
/** *
*
* @Author : Le
* @CreatDate : 2019/9/15
* **/
object test1 {
def main(args: Array[String]): Unit = {
//需求:将id不满8位,补足8位[补0]
//1、创建SparkSession
val spark = SparkSession.builder().master("local[3]").appName("test").getOrCreate()
//2、读取json文件
spark.read.json("dataset/test.json").createOrReplaceTempView("student")
//3、自定义udf
def increPrfix(id:String):String={
"0"*(8-id.length)+id
}
//4、注册udf函数
spark.udf.register("increPrfix",increPrfix _)
//5、使用
spark.sql("select increPrfix(id) id,name,clazz,score from student").show
}
UDAFtest
import org.apache.spark.sql.SparkSession
object UdafTest {
def main(args: Array[String]): Unit = {
//1、创建SparkSession
val spark = SparkSession.builder().master("local[3]").appName("test").getOrCreate()
//2、读取json文件,创建中间表
spark.read.json("data/test.json").createOrReplaceTempView("student")
//3、注册udaf函数 ,需要定义一个新的类
spark.udf.register("myAvg",new MyUdaf)
//4、使用
spark.sql("select myAvg(score) from student").show
}
}
MyUdaf.scala
import org.apache.spark.sql.Row
import org.apache.spark.sql.expressions.{MutableAggregationBuffer, UserDefinedAggregateFunction}
import org.apache.spark.sql.types.{DataType, DoubleType, IntegerType, StructType}
/**
* 自定义udaf函数,实现avg功能
*/
class MyUdaf extends UserDefinedAggregateFunction{
//输入值的类型 指定输入值的类型
override def inputSchema: StructType = {
new StructType().add("input",DoubleType)
}
//缓冲区 指在计算过程中需要用到的中间变量[需要用到两个中间变量:一个是输入的总和,一个是输入的总条数]
override def bufferSchema: StructType = {
new StructType().add("sum",DoubleType).add("total",IntegerType)
}
//指最终的计算结果的类型
override def dataType: DataType = {
DoubleType
}
//指数据的一致性 一般赋值true
override def deterministic: Boolean = true
//初始化缓冲区 也就是指定中间变量的初始值
override def initialize(buffer: MutableAggregationBuffer): Unit = {
//指定sum的初始值为0
buffer(0) = 0.0
//指定total的初始值为0
buffer(1) = 0
}
//进入一条数据就对对中间变量进行更新
override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
//更新sum = 缓冲区中的sum+传进来的score
buffer(0) = buffer.getDouble(0) + input.getDouble(0)
//更新total = 缓冲区中的total+1
buffer(1) = buffer.getInt(1) + 1
}
//合并缓冲区的数据
override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
buffer1(0) = buffer1.getDouble(0) + buffer2.getDouble(0)
buffer1(1) = buffer1.getInt(1) + buffer2.getInt(1)
}
//计算最终结果 = sum/total
override def evaluate(buffer: Row): Any = {
buffer.getDouble(0) / buffer.getInt(1)
}
}
开窗函数
import org.apache.spark.sql.SparkSession
object Window {
def main(args: Array[String]): Unit = {
/**
* 常用开窗函数:(最常用的应该是1.2.3 的排序)
* --排序函数
* 1、row_number() over(partition by ... order by ...)
* 2、rank() over(partition by ... order by ...)
* 3、dense_rank() over(partition by ... order by ...)
* --聚合函数
* 4、count() over(partition by ... order by ...)
* 5、max() over(partition by ... order by ...)
* 6、min() over(partition by ... order by ...)
* 7、sum() over(partition by ... order by ...)
* 8、avg() over(partition by ... order by ...)
* 9、first_value() over(partition by ... order by ...)
* 10、last_value() over(partition by ... order by ...)
*/
//需求: 获得班级成绩前两名的学生信息
//1、创建SparkSession
val spark = SparkSession.builder().master("local[3]").appName("test").getOrCreate()
//2、读取文件注册成临时表
spark.read.json("data/test.json").createOrReplaceTempView("student")
//3、使用开窗函数获得学生信息
//row_number rank dense_rank必须要指定order by
spark.sql(
"""
|select t.id,t.name,t.clazz,t.score from(
|select s.id,s.name,s.clazz,s.score,row_number() over(partition by s.clazz order by s.score) rn
| from student s) t where t.rn<=2
""".stripMargin).show
/**
* {"id":"003","name":"c","clazz":1,"score":95} 1
* {"id":"1","name":"a","clazz":1,"score":80} 2
* {"id":"02","name":"b","clazz":1,"score":78} 3
*
* {"id":"06","name":"e","clazz":2,"score":92} 1
* {"id":"05","name":"d","clazz":2,"score":74} 2
*
* {"id":"7","name":"f","clazz":3,"score":99} 1
* {"id":"8","name":"g","clazz":3,"score":99} 2
* {"id":"11","name":"j","clazz":3,"score":78} 3
* {"id":"10","name":"i","clazz":3,"score":55} 4
* {"id":"9","name":"h","clazz":3,"score":45} 5
*
*/
spark.sql(
"""select t.id,t.name,t.clazz,t.score from(
|select s.*,rank() over(partition by s.clazz order by s.score desc) rn
| from student s) t where t.rn<=2
""".stripMargin)//.show
/**
* +-----+---+----+-----+---+
* |clazz| id|name|score| rn|
* +-----+---+----+-----+---+
* | 1|003| c| 95| 1|
* | 1| 1| a| 80| 2|
* | 1| 02| b| 78| 3|
*
* | 3| 7| f| 99| 1|
* | 3| 8| g| 99| 1|2
* | 3| 8| g| 99| 1|3
* | 3| 11| j| 78| 4|
* | 3| 10| i| 55| 5|
* | 3| 9| h| 45| 6|
*
* | 2| 06| e| 92| 1|
* | 2| 05| d| 74| 2|
*/
spark.sql(
"""
|select s.*,dense_rank() over(partition by s.clazz order by s.score desc) rn
| from student s
""".stripMargin)//.show
/**
* +-----+---+----+-----+---+
* |clazz| id|name|score| rn|
* +-----+---+----+-----+---+
* | 1|003| c| 95| 1|
* | 1| 1| a| 80| 2|
* | 1| 02| b| 78| 3|
*
* | 3| 7| f| 99| 1|
* | 3| 8| g| 99| 1|
* | 3| 11| j| 78| 2|
* | 3| 10| i| 55| 3|
* | 3| 9| h| 45| 4|
*
* | 2| 06| e| 92| 1|
* | 2| 05| d| 74| 2|
* +-----+---+----+-----+---+
*/
//聚合与开窗函数结合的时候
// 1、聚合函数(需要指定字段)
// 2、over(可以不用指定partition by 与order by),如果不指定就是指全局
spark.sql(
"""
|select s.*,max(s.score) over() max_score
| from student s
""".stripMargin)//.show
/**
* +-----+---+----+-----+---------+
* |clazz| id|name|score|max_score|
* +-----+---+----+-----+---------+
* | 1| 1| a| 80| 95|
* | 1| 02| b| 78| 95|
* | 1|003| c| 95| 95|
* | 3| 7| f| 99| 99|
* | 3| 8| g| 99| 99|
* | 3| 9| h| 45| 99|
* | 3| 10| i| 55| 99|
* | 3| 11| j| 78| 99|
* | 2| 05| d| 74| 92|
* | 2| 06| e| 92| 92|
* +-----+---+----+-----+---------+
*/
}
}
:_* ` 把数组集合中的数据遍历到方法参数中
sql
name,price,crawl_time,market,province,city table
1、每个省份农产品市场的个数
select provice,count(distinct market) from table group by provice
2、没有农产品市场的省份
select b.* from table a right join table2 on a.province = b.province
where a.province is null
3、根据农产品类型数据,统计前三名
select name,count(1)
from table group by name
order by count(1) desc
limit 3
4、根据农产品类型数量,统计每个省份前三名
select province,name from(
select province,name,rank() over(partition by province order by count(*) desc) rn
from table
group by province,name) t where t.rn<=3
5、计算山西省每种农产品价格波动
select name,(sum(price)-max(price)-min(price))/(count(1)-2) price
from table where provice = '山西'
group by name
总结
1、读取
spark.read
format: 指定数据读取的类型
option: 指定读取时的属性:header、infreschema...
schema: 指定读取后数据的schema信息
load: 加载数据
简洁:spark.read.csv
2、写入
df.write.mode(SaveMode.Append).csv
写入模式:
SaveMode.Append:追加
SaveMode.Overwrite:覆盖
3、parquet:
读取:
1、spark.read.format("parquet").load
2、spark.read.parquet(目录名/具体文件名)
写入:
1、df.write.mode(..).parquet(目录名)
2、df.write.partitionBy(分区字段).mode.parquet
4、json:
读取:
1、spark.read.format("json").load
2、spark.read.json(目录名/具体文件名)
写入:
df.write.json(...)
将DataFrame或者DataSet转为json格式: df.toJson
读取json格式的RDD:spark.read.json(RDD[String])
5、hive
编程:
1、指定metastore的地址: hive.meatstore.uris
2、指定warehouse路径: spark.sql.warehouse.dir
3、开启hive支持: enableHiveSupport
读取:spark.sql("select * from hive表")
写入:df.write.mode.saveAsTable(hive表名)
6、mysql
读取: spark.read.jdbc(url,table,prop)
写入: df.write.mode.jdbc(url,table,prop)
7、
有类型:
1、map、flatMap、mapPartition、transform[函数只有一个参数:DataSet]
2、将DataFrame转为DataSet: df.as[待转类型]
3、filter
1、filter(函数)
2、filter(sql表达式)
3、filter(column对象)
4、groupByKey(需要指定key)
5、split(Array(5,2,3)) //Array中有几个值就分为几份,Array中的值为每一份的权重
6、orderBy
5、
distinct: 所有列的值都必须相同才能去重
dropDuplicates: 指定列的值都必须相同才能去重
6、集合
差集、交集、并集
无类型:
1、选择
1、select
2、selectExpr
2、分组
groupBy
3、Column
1、创建
1、无绑定
1、'列名 : import spark.implicats._
2、$"列名" : import spark.implicats._
3、col("列名") : import org.apache.spark.sql.functions._
4、Column("列名") : import org.apache.spark.sql.functions._
2、有绑定
1、dataset.col("列名")
2、dataset.apply("列名")
2、操作
1、别名:
col("列名") as "别名"
2、类型转换
col("列名").as[类型]
3、其他操作
like
isin
....
8、缺失值
缺失值: null、"",NaN、"Null" 等都叫缺失值
API: df.na
drop:
any:
如果一行数据中有任意一列的值为NaN或者null就删除该行
all:
如果一行数据中所有列的值全部为NaN或者null才会删除该行
针对特定列:
以上两种规则只针对指定的列
fill:
对NaN或者null的值进行填充
replace:
针对指定的值进行替换
字符串缺失值的处理:
select中使用when
where进行过滤