目录
一、SparkSQL 是什么
1.1. SparkSQL 的出现契机
数据分析的方式
1.2. SparkSQL 的适用场景
二、 SparkSQL 初体验
2.1. RDD 版本的 WordCount
2.2. 命令式 API 的入门案例
SparkSession
2.3. SQL 版本 WordCount
三、 Catalyst 优化器
3.1. RDD 和 SparkSQL 运行时的区别
3.2. Catalyst
四、Dataset 的特点
五、DataFrame 的作用和常见操作
六、 Dataset 和 DataFrame 的异同
七、 数据读写
7.1. 初识 DataFrameReader
7.2. 初识 DataFrameWriter
7.3. 读写 Parquet 格式文件
7.4. 读写 JSON 格式文件
7.5. 访问 Hive
7.5.1. SparkSQL 整合 Hive
7.5.2. 访问 Hive 表
7.6. JDBC
八、Dataset (DataFrame) 的基础操作
8.1. 有类型操作
8.2. 无类型转换
8.3. Column 对象
九、 缺失值处理
数据分析的方式
数据分析的方式大致上可以划分为
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
出现了以后, 统一了两种数据处理范式, 是一种革新性的进步.因为
SQL
是数据分析领域一个非常重要的范式, 所以Spark
一直想要支持这种范式, 而伴随着一些决策失误, 这个过程其实还是非常曲折的
Hive
解决的问题
Hive
实现了SQL on Hadoop
, 使用MapReduce
执行任务简化了
MapReduce
任务,不用写代码了新的问题
Hive
的查询延迟比较高, 原因是使用MapReduce
做调度 MR过程数据要落盘
Shar
解决的问题
Shark
改写Hive
的物理执行计划, 使用Spark
作业代替MapReduce
执行物理计划使用列式内存存储
以上两点使得
Shark
的查询效率很高新的问题
Shark
重用了Hive
的SQL
解析, 逻辑计划生成以及优化, 所以其实可以认为Shark
只是把Hive
的物理执行替换为了Spark
作业执行计划的生成严重依赖
Hive
, 想要增加新的优化非常困难
Hive
使用MapReduce
执行作业, 所以Hive
是进程级别的并行, 而Spark
是线程级别的并行, 所以Hive
中很多线程不安全的代码不适用于Spark
由于以上问题,
Shark
维护了Hive
的一个分支, 并且无法合并进主线, 难以为继
SparkSQL
解决的问题
Spark SQL
使用Hive
解析SQL
生成AST
语法树, 将其后的逻辑计划生成, 优化, 物理计划都自己完成, 而不依赖Hive
执行计划和优化交给优化器
Catalyst
内建了一套简单的
SQL
解析器, 可以不使用HQL
, 此外, 还引入和DataFrame
这样的DSL API
, 完全可以不依赖任何Hive
的组件
Shark
只能查询文件,Spark SQL
可以直接将查询作用于RDD
, 这一点是一个大进步新的问题
对于初期版本的
SparkSQL
, 依然有挺多问题, 例如只能支持SQL
的使用, 不能很好的兼容命令式, 入口不够统一等
Dataset
SparkSQL
在 2.0 时代, 增加了一个新的API
, 叫做Dataset
,Dataset
统一和结合了SQL
的访问和命令式API
的使用, 这是一个划时代的进步在
Dataset
中可以轻易的做到使用SQL
查询并且筛选数据, 然后使用命令式API
进行探索式分析
重要性
SparkSQL
不只是一个SQL
引擎,SparkSQL
也包含了一套对 结构化数据的命令式API
, 事实上, 所有Spark
中常见的工具, 都是依赖和依照于SparkSQL
的API
设计的
总结: SparkSQL
是什么
SparkSQL
是一个为了支持SQL
而设计的工具, 但同时也支持命令式的API
常见的几种数据格式
定义 | 特点 | 举例 | |
---|---|---|---|
结构化数据 |
有固定的 |
有预定义的 |
关系型数据库的表 |
半结构化数据 |
没有固定的 |
没有固定的 |
指一些有结构的文件格式, 例如 |
非结构化数据 |
没有固定 |
没有固定 |
指文档图片之类的格式 |
结构化数据
一般指数据有固定的 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
的, 可以理解为没有显式指定Schema
比如说一个用户信息的JSON
文件, 第一条数据的phone_num
有可能是String
, 第二条数据虽说应该也是String
, 但是如果硬要指定为BigInt
, 也是有可能的
因为没有指定Schema
, 没有显式的强制的约束
有结构
虽说半结构化数据是没有显式指定
Schema
的, 也没有约束, 但是半结构化数据本身是有有隐式的结构的, 也就是数据自身可以描述自身
例如JSON
文件, 其中的某一条数据是有字段这个概念的, 每个字段也有类型的概念, 所以说JSON
是可以描述自身的, 也就是数据本身携带有元信息
SparkSQL
处理什么数据的问题?
Spark
的 RDD
主要用于处理 非结构化数据 和 半结构化数据
SparkSQL
主要用于处理 结构化数据
SparkSQL
相较于 RDD
的优势在哪?
SparkSQL
提供了更好的外部数据源读写支持
RDD
之外有一个新的解决方案, 来整合这些结构化数据源SparkSQL
提供了直接访问列的能力
SparkSQL
主要用做于处理结构化数据, 所以其提供的 API
具有一些普通数据库的能力总结: SparkSQL
适用于什么场景?
SparkSQL
适用于处理结构化数据的场景
目标
了解 SparkSQL
的 API
由哪些部分组成
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
版本的代码有一个非常明显的特点, 就是它所处理的数据是基本类型的, 在算子中对整个数据进行处理
package com.kangna.exe
import org.apache.spark.SparkContext
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{DataFrame, Dataset, Row, SparkSession}
import org.junit.Test
case class Person(id: Int, name: String, age: Int)
class SparkCoreExe {
private val spark: SparkSession = SparkSession.builder().appName("DataSetDemo").master("local[8]").getOrCreate()
private val sc: SparkContext = spark.sparkContext
import spark.implicits._
@Test
def DS_1() : Unit = {
val peopleRDD: RDD[Person] = sc.parallelize(Seq(Person(110, "kangna", 23), Person(120, "lisi", 34)))
val personDS: Dataset[Person] = peopleRDD.toDS()
val teenagers: Dataset[String] = personDS.where('age > 10)
.where('age < 30)
.select('name)
.as[String]
teenagers.show()
}
}
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
等不同的入口点为了支持更多的数据源, 应该完善读取和写入体系
同时对于原来的入口点也不能放弃, 要向下兼容
DataFrame & Dataset
SparkSQL
最大的特点就是它针对于结构化数据设计, 所以SparkSQL
应该是能支持针对某一个字段的访问的, 而这种访问方式有一个前提, 就是SparkSQL
的数据集中, 要 包含结构化信息, 也就是俗称的Schema
而 SparkSQL
对外提供的 API
有两类, 一类是直接执行 SQL
, 另外一类就是命令式. SparkSQL
提供的命令式 API
就是 DataFrame
和 Dataset
, 暂时也可以认为 DataFrame
就是 Dataset
, 只是在不同的 API
中返回的是 Dataset
的不同表现形式
// RDD
rdd.map { case Person(id, name, age) => (age, 1) }
.reduceByKey {case ((age, count), (totalAge, totalCount)) => (age, count + totalCount)}
// DataFrame
df.groupBy("age").count("age")
通过上面的代码, 可以清晰的看到, SparkSQL
的命令式操作相比于 RDD
来说, 可以直接通过 Schema
信息来访问其中某个字段, 非常的方便
@Test
def DS_2() : Unit = {
val peopleRDD: RDD[Person] = sc.parallelize(Seq(Person(110, "kangna", 23), Person(120, "lisi", 34)))
val personDS: Dataset[Person] = peopleRDD.toDS()
// Creates a local temporary view using the given name
personDS.createOrReplaceTempView("people")
val person: DataFrame = spark.sql(
"""
|select name from people where age > 20 and age < 30
""".stripMargin)
person.show()
}
以往使用
SQL
肯定是要有一个表的, 在Spark
中, 并不存在表的概念, 但是有一个近似的概念, 叫做DataFrame
, 所以一般情况下要先通过DataFrame
或者Dataset
注册一张临时表, 然后使用SQL
操作这张临时表
总结
SparkSQL
提供了 SQL
和 命令式 API
两种不同的访问结构化数据的形式, 并且它们之间可以无缝的衔接API
由一个叫做 Dataset
的组件提供, 其还有一个变形, 叫做 DataFrame
目标
理解 SparkSQL
和以 RDD
为代表的 SparkCore
最大的区别
理解优化器的运行原理和作用
RDD
的运行流程
大致运行步骤
先将
RDD
解析为由Stage
组成的DAG
, 后将Stage
转为Task
直接运行
问题
任务会按照代码所示运行, 依赖开发者的优化, 创建一个组件, 帮助开发者修改和优化代码, 但是这在 RDD
上是无法实现的
为什么 RDD
无法自我优化?
RDD
没有 Schema
信息
RDD
可以同时处理结构化和非结构化的数据
SparkSQL
提供了什么?
和 RDD
不同, SparkSQL
的 Dataset
和 SQL
并不是直接生成计划交给集群执行, 而是经过了一个叫做 Catalyst
的优化器, 这个优化器能够自动帮助开发者优化代码
也就是说, 在 SparkSQL
中, 开发者的代码即使不够优化, 也会被优化为相对较好的形式去执行
为什么 SparkSQL
提供了这种能力?
首先, SparkSQL
大部分情况用于处理结构化数据和半结构化数据, 所以 SparkSQL
可以获知数据的 Schema
, 从而根据其 Schema
来进行优化
为了解决过多依赖 Hive
的问题, SparkSQL
使用了一个新的 SQL
优化器替代 Hive
中的优化器, 这个优化器就是 Catalyst
, 整个 SparkSQL
的架构大致如下
API
层简单的说就是 Spark
会通过一些 API
接受 SQL
语句
收到 SQL
语句以后, 将其交给 Catalyst
, Catalyst
负责解析 SQL
, 生成执行计划等
Catalyst
的输出应该是 RDD
的执行计划
最终交由集群运行
Step 1 : 解析 SQL
, 并且生成 AST
(抽象语法树)
Step 2 : 在 AST
中加入元数据信息, 做这一步主要是为了一些优化, 例如 col = col
这样的条件, 下图是一个简略图, 便于理解
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 : 对已经加入元数据的 AST
, 输入优化器, 进行优化, 从两种常见的优化开始, 简单介绍
谓词下推 Predicate Pushdown
, 将 Filter
这种可以减小数据集的操作下推, 放在 Scan
的位置, 这样可以减少操作时候的数据量
列值裁剪 Column Pruning
, 在谓词下推后, people
表之上的操作只用到了 id
列, 所以可以把其它列裁剪掉, 这样可以减少处理的数据量, 从而优化处理速度
还有其余很多优化点, 大概一共有一二百种, 随着 SparkSQL
的发展, 还会越来越多, 感兴趣的可以继续通过源码了解, 源码在 org.apache.spark.sql.catalyst.optimizer.Optimizer
Step 4 : 上面的过程生成的 AST
其实最终还没办法直接运行, 这个 AST
叫做 逻辑计划
, 结束后, 需要生成 物理计划
, 从而生成 RDD
来运行
在生成`物理计划`的时候, 会经过`成本模型`对整棵树再次执行优化, 选择一个更好的计划
在生成`物理计划`以后, 因为考虑到性能, 所以会使用代码生成, 在机器中运行
- 可以使用
queryExecution
方法查看逻辑执行计划, 使用explain
方法查看物理执行计划( SQL 也有此处的词法,语法树,编译原理的知识)- 也可以使用
Spark WebUI
进行查看
总结
SparkSQL
和 RDD
不同的主要点是在于其所操作的数据是结构化的, 提供了对数据更强的感知和分析能力, 能够对代码进行更深层的优化, 而这种能力是由一个叫做 Catalyst
的优化器所提供的
Catalyst
的主要运作原理是分为三步, 先对SQL
或者Dataset
的代码解析, 生成逻辑计划, 后对逻辑计划进行优化, 再生成物理计划, 最后生成代码到集群中以RDD
的形式运行
目标
理解 Dataset
是什么
理解 Dataset
的特性
Dataset
是什么?
问题1:
People
是什么?
People
是一个 强类型 的类问题2:
Dataset
中是结构化的数据吗?非常明显是的, 因为
People
对象中有结构信息, 例如字段名和字段类型问题3:
Dataset
能够使用类似SQL
这样声明式结构化查询语句的形式来查询吗?当然可以
问题4:
Dataset
是什么?
Dataset
是一个强类型, 并且类型安全的数据容器, 并且提供了结构化查询API
和类似RDD
一样的命令式API
即使使用 Dataset
的命令式 API
, 执行计划也依然会被优化
Dataset
具有RDD
的方便, 同时也具有DataFrame
的性能优势, 并且Dataset
还是强类型的, 能做到类型安全.scala> spark.range(1).filter('id === 0).explain(true) == 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
最底层处理的是对象的序列化形式, 通过查看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[]--- Encoder ----> InternalRow
输入----InterRow---- Decoder-----> 泛型对象
可以获取 Dataset
对应的 RDD
表示
在 Dataset
中, 可以使用一个属性 rdd
来得到它的 RDD
表示, 例如 Dataset[T] → RDD[T]
Dataset.rdd
将 Dataset
转为 RDD
的形式Dataset
的执行计划底层的 RDD
总结
Dataset
是一个新的Spark
组件, 其底层还是RDD
Dataset
提供了访问对象中某个特定字段的能力, 不用像RDD
一样每次都要针对整个对象做操作
Dataset
和RDD
不同, 如果想把Dataset[T]
转为RDD[T]
, 则需要对Dataset
底层的InternalRow
做转换, 是一个比价重量级的操作
目标
理解 DataFrame
是什么
理解 DataFrame
的常见操作
DataFrame
是什么?
DataFrame
是SparkSQL
中一个表示关系型数据库中表
的函数式抽象, 其作用是让Spark
处理大规模结构化数据的时候更加容易. 一般DataFrame
可以处理结构化的数据, 或者是半结构化的数据, 因为这两类数据中都可以获取到Schema
信息. 也就是说DataFrame
中有Schema
信息, 可以像操作表一样操作DataFrame
.
DataFrame
(row+Schema)由两部分构成, 一是row
的集合, 每个row
对象表示一个行, 二是描述DataFrame
结构的Schema
.
DataFrame
支持SQL
中常见的操作, 例如:select
,filter
,join
,group
,sort
,join
等
通过隐式转换创建 DataFrame
这种方式本质上是使用
SparkSession
中的隐式转换来进行的val spark: SparkSession = new sql.SparkSession.Builder() .appName("hello") .master("local[6]") .getOrCreate() // 必须要导入隐式转换 // 注意: spark 在此处不是包, 而是 SparkSession 对象 import spark.implicits._ val peopleDF: DataFrame = Seq(People("zhangsan", 15), People("lisi", 15)).toDF()
根据源码可以知道,
toDF
方法可以在RDD
和Seq
中使用通过集合创建
DataFrame
的时候, 集合中不仅可以包含样例类, 也可以只有普通数据类型, 后通过指定列名来创建@Test def DS_6() : Unit = { // 必须要导入隐式转换 // 注意: spark 在此处不是包, 而是 SparkSession 对象 import spark.implicits._ val df1: DataFrame = Seq("nihao", "hello").toDF("text") df1.show() val df2: DataFrame = Seq(("a", 1), ("b", 1)).toDF("word", "count") df2.show() }
通过外部集合创建 DataFrame
数据
No,year,month,day,hour,season,PM_Dongsi,PM_Dongsihuan,PM_Nongzhanguan,PM_US_Post,DEWP,HUMI,PRES,TEMP,cbwd,Iws,precipitation,Iprec 1,2010,1,1,0,4,NA,NA,NA,NA,-21,43,1021,-11,NW,1.79,0,0 2,2010,1,1,1,4,NA,NA,NA,NA,-21,47,1020,-12,NW,4.92,0,0 3,2010,1,1,2,4,NA,NA,NA,NA,-21,43,1019,-11,NW,6.71,0,0 4,2010,1,1,3,4,NA,NA,NA,NA,-21,55,1019,-14,NW,9.84,0,0 5,2010,1,1,4,4,NA,NA,NA,NA,-20,51,1018,-12,NW,12.97,0,0 6,2010,1,1,5,4,NA,NA,NA,NA,-19,47,1017,-10,NW,16.1,0,0 7,2010,1,1,6,4,NA,NA,NA,NA,-19,44,1017,-9,NW,19.23,0,0
@Test def DS_7() : Unit = { // 必须要导入隐式转换 // 注意: spark 在此处不是包, 而是 SparkSession 对象 import spark.implicits._ val dataFrame: DataFrame = spark.read.option("header", true) .csv("dataset/BeijingPM20100101_20151231.csv") dataFrame dataFrame.show(10) dataFrame.printSchema() }
不仅可以从
csv
文件创建DataFrame
, 还可以从Table
,JSON
,Parquet
等中创建DataFrame
, 后续会有介绍
在 DataFrame
上可以使用的常规操作
需求: 查看每个月的统计数量
Step 1: 首先可以打印 DataFrame
的 Schema
, 查看其中所包含的列, 以及列的类型
// 必须要导入隐式转换
// 注意: spark 在此处不是包, 而是 SparkSession 对象
import spark.implicits._
val dataFrame: DataFrame = spark.read.option("header", true)
.csv("dataset/BeijingPM20100101_20151231.csv")
dataFrame
dataFrame.show(10)
dataFrame.printSchema()
root
|-- No: string (nullable = true)
|-- year: string (nullable = true)
|-- month: string (nullable = true)
|-- day: string (nullable = true)
|-- hour: string (nullable = true)
|-- season: string (nullable = true)
|-- PM_Dongsi: string (nullable = true)
|-- PM_Dongsihuan: string (nullable = true)
|-- PM_Nongzhanguan: string (nullable = true)
|-- PM_US_Post: string (nullable = true)
|-- DEWP: string (nullable = true)
|-- HUMI: string (nullable = true)
|-- PRES: string (nullable = true)
|-- TEMP: string (nullable = true)
|-- cbwd: string (nullable = true)
|-- Iws: string (nullable = true)
|-- precipitation: string (nullable = true)
|-- Iprec: string (nullable = true)
Step 2: 对于大部分计算来说, 可能不会使用所有的列, 所以可以选择其中某些重要的列
Step 3: 可以针对某些列进行分组, 后对每组数据通过函数做聚合
@Test
def DS_7() : Unit = {
// 必须要导入隐式转换
// 注意: spark 在此处不是包, 而是 SparkSession 对象
import spark.implicits._
val dataFrame: DataFrame = spark.read.option("header", true)
.csv("dataset/BeijingPM20100101_20151231.csv")
dataFrame
dataFrame.select('year, 'month, 'PM_Dongsi) // 选择想要的列
.where( 'PM_Dongsi =!= "Na" ) // 过滤
.groupBy('year, 'month) // 分组
.count() // 求和
.show()
}
使用 SQL
操作 DataFrame
使用 SQL
来操作某个 DataFrame
的话, SQL
中必须要有一个 from
子句, 所以需要先将 DataFrame
注册为一张临时表
总结
DataFrame
是一个类似于关系型数据库表的函数式组件
DataFrame
一般处理结构化数据和半结构化数据
DataFrame
具有数据对象的 Schema 信息
可以使用命令式的 API
操作 DataFrame
, 同时也可以使用 SQL
操作 DataFrame
DataFrame
可以由一个已经存在的集合直接创建, 也可以读取外部的数据源来创建
目标
理解 Dataset
和 DataFrame
之间的关系
DataFrame
就是 Dataset
根据前面的内容, 可以得到如下信息
Dataset
中可以使用列来访问数据,DataFrame
也可以
Dataset
的执行是优化的,DataFrame
也是
Dataset
具有命令式API
, 同时也可以使用SQL
来访问,DataFrame
也可以使用这两种不同的方式访问所以这件事就比较蹊跷了, 两个这么相近的东西为什么会同时出现在
SparkSQL
中呢?确实, 这两个组件是同一个东西,
DataFrame
是Dataset
的一种特殊情况, 也就是说DataFrame
是Dataset[Row]
的别名
DataFrame
和 Dataset
所表达的语义不同
第一点:
DataFrame
表达的含义是一个支持函数式操作的表
, 而Dataset
表达是是一个类似RDD
的东西,Dataset
可以处理任何对象第二点:
DataFrame
中所存放的是Row
对象, 而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
对象, 如果调用错了方法, 编译时就会被检查出来(DataFrame 会被放的是Row, 编译时类型擦除 应该可以这么理解)val ds: Dataset[People] = Seq(People("zhangsan", 15), People("lisi", 15)).toDS() ds.map(person => person.hello)
这行代码明显报错, 无法通过编译
Row
是什么?
Row
对象表示的是一个行
Row
的操作类似于Scala
中的Map
数据类型// 一个对象就是一个对象 val p = People(name = "zhangsan", age = 10) // 同样一个对象, 还可以通过一个 Row 对象来表示 val row = Row("zhangsan", 10) // 获取 Row 中的内容 println(row.get(1)) println(row(1)) // 获取时可以指定类型 println(row.getAs[Int](1)) // 同时 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
, 是一个行的概念
目标
理解外部数据源的访问框架
掌握常见的数据源读写方式
目标:理解 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
等但是其实这两种方式本质上一样, 因为类似 csv
这样的方式只是 load
的封装
load
方法加载数据, 但是没有指定 format
的话, 默认是按照 Parquet
文件格式读取 也就是说, SparkSQL
默认的读取格式是 Parquet
总结
使用
spark.read
可以获取 SparkSQL 中的外部数据源访问框架DataFrameReader
DataFrameReader
有三个组件format
,schema
,option
DataFrameReader
有两种使用方式, 一种是使用load
加format
指定格式, 还有一种是使用封装方法csv
,json
等
目标
理解 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
等
@Test
def DS_10(): Unit = {
import spark.implicits._
// 使用load 方法
val df: DataFrame = spark.read.format("csv")
.option("header", true)
.load("dataset/BeijingPM20100101_20151231.csv")
// 使用 save 保存, 使用 format 设置文件格式
df.write.format("json").save("data/beijingPM")
// 使用 json 保存,因违反方法是 json, 所以隐含的 fromat 是 json
df.write.json("data/beijingPM")
}
默认没有指定 format
, 默认的 format
是 Parquet
总结
类似 DataFrameReader
, Writer
中也有 format
, options
, 另外 schema
是包含在 DataFrame
中的
DataFrameWriter
中还有一个很重要的概念叫做 mode
, 指定写入模式, 如果目标集合已经存在时的行为
DataFrameWriter
可以将数据保存到 Hive
表中, 所以也可以指定分区和分桶信息
目标
理解 Spark
读写 Parquet
文件的语法
理解 Spark
读写 Parquet
文件的时候对于分区的处理
什么时候会用到 Parquet
?
在
ETL
中,Spark
经常扮演T
的职务, 也就是进行数据清洗和数据转换.为了能够保存比较复杂的数据, 并且保证性能和压缩率, 通常使用
Parquet
是一个比较不错的选择.所以外部系统收集过来的数据, 有可能会使用
Parquet
, 而Spark
进行读取和转换的时候, 就需要支持对Parquet
格式的文件的支持.
使用代码读写 Parquet
文件
默认不指定
format
的时候, 默认就是读写Parquet
格式的文件@Test def parquetSaveFile(): Unit = { import spark.implicits._ // 使用load 方法 val df: DataFrame = spark.read.format("csv") .option("header", true) .csv("dataset/BeijingPM20100101_20151231.csv") // 保存 Parquet 文件 df.write.mode("overwrite").save("dataset/BeiJing.parquet") // 读取 Parquet 文件 val dfFromParquet: DataFrame = spark.read.parquet("dataset/BeiJing.parquet") dfFromParquet.createOrReplaceTempView("BeiJing_tempTable") val dataFrame: DataFrame = spark.sql( """ | select year,month,count(*) as cnt | from BeiJing_tempTable where PM_Dongsi != 'NA' | group by year,month """.stripMargin) dataFrame dataFrame.show() }
写入 Parquet
的时候可以指定分区
Spark
在写入文件的时候是支持分区的, 可以像 Hive
一样设置某个列为分区列
@Test
def DS_10(): Unit = {
import spark.implicits._
// 使用load 方法
val df: DataFrame = spark.read.format("csv")
.option("header", true)
.load("dataset/BeijingPM20100101_20151231.csv")
// 保存为 Parquet 格式文件,不指定 format 默认就是 Parquet
df.write.partitionBy("year","month").save("dataset/xiAn_pm")
}
这个地方指的分区是类似 Hive
中表分区的概念, 而不是 RDD
分布式分区的含义
分区发现
在读取常见文件格式的时候, Spark
会自动的进行分区发现, 分区自动发现的时候, 会将文件名中的分区信息当作一列. 例如 如果按照性别分区, 那么一般会生成两个文件夹 gender=male
和 gender=female
, 那么在使用 Spark
读取的时候, 会自动发现这个分区信息, 并且当作列放入创建的 DataFrame
中
使用代码证明这件事可以有两个步骤, 第一步先读取某个分区的单独一个文件并打印其 Schema
信息, 第二步读取整个数据集所有分区并打印 Schema
信息, 和第一步做比较就可以确定
val spark = ...
val partDF = spark.read.load("dataset/beijing_pm/year=2010/month=1")
partDF.printSchema()
val df = spark.read.load("dataset/beijing_pm")
df.printSchema()
配置 | 默认值 | 含义 |
---|---|---|
|
|
一些其他 |
|
|
一些其他 |
|
|
打开 Parquet 元数据的缓存, 可以加快查询静态数据(如:导出的脱敏数据) |
|
|
压缩方式, 可选 |
|
|
当为 true 时, Parquet 数据源会合并从所有数据文件收集的 Schemas 和数据, 因为这个操作开销比较大, 所以默认关闭 |
|
|
如果为 |
总结
Spark
不指定 format
的时候默认就是按照 Parquet
的格式解析文件
Spark
在读取 Parquet
文件的时候会自动的发现 Parquet
的分区和分区字段
Spark
在写入 Parquet
文件的时候如果设置了分区字段, 会自动的按照分区存储
目标
理解 JSON
的使用场景
能够使用 Spark
读取处理 JSON
格式文件
什么时候会用到 JSON
?
在
ETL
中,Spark
经常扮演T
的职务, 也就是进行数据清洗和数据转换.在业务系统中,
JSON
是一个非常常见的数据格式, 在前后端交互的时候也往往会使用JSON
, 所以从业务系统获取的数据很大可能性是使用JSON
格式, 所以就需要Spark
能够支持 JSON 格式文件的读取
读写 JSON
文件
将要
Dataset
保存为JSON
格式的文件比较简单, 是DataFrameWriter
的一个常规使用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
beijing_pm.jsonl
{"day":"1","hour":"0","season":"1","year":2013,"month":3} {"day":"1","hour":"1","season":"1","year":2013,"month":3} {"day":"1","hour":"2","season":"1","year":2013,"month":3}
也可以通过
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
这种情况其实还是比较常见的, 例如如下的流程
假设业务系统通过 Kafka
将数据流转进入大数据平台, 这个时候可能需要使用 RDD
或者 Dataset
来读取其中的内容, 这个时候一条数据就是一个 JSON
格式的字符串, 如何将其转为 DataFrame
或者 Dataset[Object]
这样具有 Schema
的数据集呢? 使用如下代码就可以
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
, 使用 Hive
的 MetaStore
元信息库
使用 SparkSQL
查询 Hive
表
案例, 使用常见 HiveSQL
写入内容到 Hive
表
导读
开启 Hive
的 MetaStore
独立进程
整合 SparkSQL
和 Hive
的 MetaStore
和一个文件格式不同, Hive
是一个外部的数据存储和查询引擎, 所以如果 Spark
要访问 Hive
的话, 就需要先整合 Hive
整合什么 ?
如果要讨论
SparkSQL
如何和Hive
进行整合, 首要考虑的事应该是Hive
有什么, 有什么就整合什么就可以
MetaStore
, 元数据存储
SparkSQL
内置的有一个MetaStore
, 通过嵌入式数据库Derby
保存元信息, 但是对于生产环境来说, 还是应该使用Hive
的MetaStore
, 一是更成熟, 功能更强, 二是可以使用Hive
的元信息查询引擎
SparkSQL
内置了HiveSQL
的支持, 所以无需整合
为什么要开启 Hive
的 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
的进程(Spark onYarn 的 client 模式 差不多 , 客户端 出问题 程序运行报错)
Remote
模式和
Loca
模式一样, 访问MySQL
数据库存放元数据, 但是Remote
的MetaStore
运行在独立的进程中我们显然要选择
Remote
模式, 因为要让其独立运行, 这样才能让SparkSQL
一直可以访问
Hive
开启 MetaStore
Step 1
: 修改hive-site.xml
hive.metastore.warehouse.dir /user/hive/warehouse javax.jdo.option.ConnectionURL jdbc:mysql://node01:3306/hive?createDatabaseIfNotExist=true javax.jdo.option.ConnectionDriverName com.mysql.jdbc.Driver javax.jdo.option.ConnectionUserName username javax.jdo.option.ConnectionPassword password hive.metastore.local false hive.metastore.uris thrift://node01:9083 //当前服务器
Step 2
: 启动Hive MetaStore
nohup /export/servers/hive/bin/hive --service metastore 2>&1 >> /var/log.log &
SparkSQL
整合 Hive
的 MetaStore
即使不去整合 MetaStore
, Spark
也有一个内置的 MateStore
, 使用 Derby
嵌入式数据库保存数据, 但是这种方式不适合生产环境, 因为这种模式同一时间只能有一个 SparkSession
使用, 所以生产环境更推荐使用 Hive
的 MetaStore
SparkSQL
整合 Hive
的 MetaStore
主要思路就是要通过配置能够访问它, 并且能够使用 HDFS
保存 WareHouse
, 这些配置信息一般存在于 Hadoop
和 HDFS
的配置文件中, 所以可以直接拷贝 Hadoop
和 Hive
的配置文件到 Spark
的配置目录
// Spark 需要 hive-site.xml 的原因是, 要读取 Hive 的配置信息, 主要是元数据仓库的位置等信息
// Spark 需要 core-site.xml 的原因是, 要读取安全有关的配置
// Spark 需要 hdfs-site.xml 的原因是, 有可能需要在 HDFS 中放置表文件, 所以需要 HDFS 的配置
cd /export/servers/hadoop/etc/hadoop
cp hive-site.xml core-site.xml hdfs-site.xml /export/servers/spark/conf/
如果不希望通过拷贝文件的方式整合 Hive, 也可以在 SparkSession 启动的时候, 通过指定 Hive 的 MetaStore 的位置来访问, 但是更推荐整合的方式
步骤
在 Hive
中创建表
使用 SparkSQL
访问 Hive
中已经存在的表
使用 SparkSQL
创建 Hive
表
使用 SparkSQL
修改 Hive
表中的数据
在 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
目录class SparkSQL { private val spark: SparkSession = SparkSession.builder().appName("DataSetDemo") // 设置 warehouse 路径 .config("spark.sql.warehouse.dir", "hdfs://node01:8020/dataset/hive") // Hive 元数据的地址 .config("hive.metastore.uris", "thrift://node03:9083") // 开启 Hive的 支持 .enableHiveSupport() .master("local[8]").getOrCreate() private val sc: SparkContext = spark.sparkContext @Test def hiveTableStudent(): Unit = { spark.sql("use mytest") val resultDF: DataFrame = spark.sql( """ |select * from student limit 5 """.stripMargin) resultDF.show() } }
beeline中 查询
SparkSQL查询结果
通过 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
依赖
org.apache.spark spark-hive_2.11 ${spark.version} 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
的位置
配置好了以后, 就可以通过
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 |
导读
通过 SQL
操作 MySQL
的表
将数据写入 MySQL
的表中
准备 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
中的代码如下@Test def sparkSqlToMySQL(): Unit = { // 定义表的额schema信息 val schema = StructType( List( StructField("name", StringType), StructField("age", IntegerType), StructField("gpa", FloatType) ) ) val studentDF: DataFrame = spark.read.option("delimiter", "\t") .schema(schema) .csv("dataset/studenttab10k") studentDF studentDF.write.mode(SaveMode.Overwrite).format("jdbc") .option("url", "jdbc:mysql://node03:3306/spark_test") .option("dbtable", "student") .option("user", "root") .option("password", "123456") .save() }
运行程序
如果是在本地运行, 需要导入
Maven
依赖
mysql mysql-connector-java 5.1.47 如果使用
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
也提供了针对任意类型的列作为分区依据的方法@Test def sparkSqlFromMySQLRead(): Unit = { val predicates = Array( "age < 20", "age >= 20, age < 30", "age >= 30" ) val connectionProperties = new Properties() connectionProperties.setProperty("user", "root") connectionProperties.setProperty("password", "123456") spark.read .jdbc( url = "jdbc:mysql://node03: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://node03: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
就是 Dataset
, 所以这些操作大部分也适用于 DataFrame
有类型的转换操作
无类型的转换操作
基础 Action
空值如何处理
统计操作
分类 | 算子 | 解释 |
---|---|---|
选择 |
|
|
|
在 |
|
|
通过
|
|
|
修改列名 |
|
剪除 |
drop |
剪掉某个列 |
聚合 |
groupBy |
按照给定的行进行分组 |
导读
Column 表示了 Dataset 中的一个列, 并且可以持有一个表达式, 这个表达式作用于每一条数据, 对每条数据都生成一个值, 之所以有单独这样的一个章节是因为列的操作属于细节, 但是又比较常见, 会在很多算子中配合出现
分类 | 操作 | 解释 |
---|---|---|
创建 |
|
单引号 |
|
同理, |
|
|
|
|
|
|
|
|
前面的 |
|
|
可以通过
|
|
别名和转换 |
|
|
|
通过 |
|
添加列 |
|
通过 |
操作 |
|
通过 |
|
通过 |
|
|
在排序的时候, 可以通过 |
导读
DataFrame
中什么时候会有无效值
DataFrame
如何处理无效的值
DataFrame
如何处理 null
缺失值的处理思路
如果想探究如何处理无效值, 首先要知道无效值从哪来, 从而分析可能产生的无效值有哪些类型, 在分别去看如何处理无效值
什么是缺失值
一个值本身的含义是这个值不存在则称之为缺失值, 也就是说这个值本身代表着缺失, 或者这个值本身无意义, 比如说
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()