1. SparkSQL简介
SparkSQL,可以简单的理解为Spark生态体系中用于处理结构化数据的模块。
1.1. 特点
- 可集成
- 统一的访问数据方式
- 集成Hive操作
- 提供标准的jdbc/odbc的数据库连接方式
1.2. 参考网址
官网:http://spark.apache.org/sql
1.3. 发展
在spark生态体系中,最早并不叫sparksql,最早叫shark,shark底层的任务的解析器、优化器、执行器等等都是基于hive的,所以说hive的发展制约了shark。shark的负责人意识到了这个问题之后,大概在2015年左右终止了shark的开发,从新开始搭建一套框架sparksql,同时在hive的基础之上发展出了另一套hive集成spark的框架hive-on-spark(https://cwiki.apache.org/confluence/display/Hive/Hive+on+Spark%3A+Getting+Started) 虽然shark的源代码被废弃了,但是一些非常优秀的设计思想还是被保留下来了,比如基于内存的列存储,动态字节码技术等等。
1.4. DataFrame and Datasets and RDD
类似于sparkCore中的RDD,在sparksql中的编程模型被称为之DataFrame和Datasets。DataFrame是spark1.6以前的唯一的编程模型,理解为一张二维表,或者更加简单的理解为RDD+列;Dataset是1.6之后出现的新的编程模型,集成RDD和DataFrame的优点(RDD强类型推断,强大的lambda表达式进行函数式编程;DataFrame的二维表结构,以及传统的sparksql的优化器),逐渐成为当前sparksql编程的主流。
有时候我们把RDD称之为Spark的第一代编程模型,官方预计在Spark3.0之后不再维护RDD;DataFrame被称之为Spark的第二代编程模型;Dataset被称为Spark的第三代编程模型。
这三者的比较,参见Spark-Dataset.docx
2. SparkSession的创建和初始化
2.1. SparkSQL项目的创建
选择maven的archetype骨架
指定artifactId
添加maven骨架加载方式,基于local,也就是本地仓库
![1561532661932]
模块存储路径
删除选中的几个类
添加maven依赖操作:
在父模块中加入如下依赖:
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.11</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-hive_2.11</artifactId>
<version>${spark.version}</version>
</dependency>
然后在spark-sql的模块设置为如下依赖:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>spark-parent</artifactId>
<groupId>com.desheng.bigdata</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.desheng.bigdata</groupId>
<artifactId>spark-sql</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.11</artifactId>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.11</artifactId>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-hive_2.11</artifactId>
</dependency>
</dependencies>
<build>
<sourceDirectory>src/main/scala</sourceDirectory>
<testSourceDirectory>src/test/scala</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.scala-tools</groupId>
<artifactId>maven-scala-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
<configuration>
<scalaVersion>${scala.version}</scalaVersion>
<args>
<arg>-target:jvm-1.5</arg>
</args>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-eclipse-plugin</artifactId>
<configuration>
<downloadSources>true</downloadSources>
<buildcommands>
<buildcommand>ch.epfl.lamp.sdt.core.scalabuilder</buildcommand>
</buildcommands>
<additionalProjectnatures>
<projectnature>ch.epfl.lamp.sdt.core.scalanature</projectnature>
</additionalProjectnatures>
<classpathContainers>
<classpathContainer>org.eclipse.jdt.launching.JRE_CONTAINER</classpathContainer>
<classpathContainer>ch.epfl.lamp.sdt.launching.SCALA_CONTAINER</classpathContainer>
</classpathContainers>
</configuration>
</plugin>
</plugins>
</build>
<reporting>
<plugins>
<plugin>
<groupId>org.scala-tools</groupId>
<artifactId>maven-scala-plugin</artifactId>
<configuration>
<scalaVersion>${scala.version}</scalaVersion>
</configuration>
</plugin>
</plugins>
</reporting>
</project>
2.2. SparkSession及其入门案例操作
在老版本中的SparkSQL的编程入口称之为SQLContext(通用)/HiveContext(只能操作Hive),在spark2.0以后对这两个Context做了统一,这个统一就是今天学习SparkSession。SparkSession的构建依赖SparkConf,我们可以基于SparkSession来获得SparkContext,或者SQLContext或者HiveContext。
通用的SQLContext支持通用的SQL操作,但是Hive中的一些特殊方言,SQLContext无法支持,比如row_number开窗函数,此时用方言的就是用HiveContext。
val spark = SparkSession.builder().build()
object _01SparkSQLFirstExperienceOps {
def main(args: Array[String]): Unit = {
Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
Logger.getLogger("org.apache.hadoop").setLevel(Level.WARN)
Logger.getLogger("org.spark_project").setLevel(Level.WARN)
val spark:SparkSession = SparkSession.builder()
.appName("SparkSQLFirstExperience")
.master("local[2]")
.getOrCreate()
//加载外部数据
val people:DataFrame = spark.read.json("file:///E:/data/spark/sql/people.json")
//元数据信息 show create table people;
println("=================查看该二维表的元数据信息=====================")
people.printSchema()
println("=================查看该二维表的数据=====================")
// people.foreach(row => println(row))
people.show(8)//show()打印dataframe中的前20条记录,show(num)
//查询某一列或者某几列的字段数据select name, age from xxx
println("=================查看该二维表某几列的数据=====================")
people.select("name", "age").show()
people.select(new Column("height")).show()
println("=================简单的查询操作=====================")
//比如对所有人的年龄+1 select name, age+10 as age from xxx
people.select(new Column("name"), new Column("age").+(10).as("age")).show()
/*
比如查询身高超过168的信息
gt(great than) >
lt(low than) <
gte(great or equal than) >=
lte(low or equal than) <=
eq (equal) =
neq (not equal) !=
select name, age, height > 168 from xxx
*/
people.select(new Column("name"), new Column("age"), new Column("height").>(168).as("great_than_168")).show()
// select name, age, height from xxx where height > 168
people.select(new Column("name"), new Column("age"), new Column("height"))
// .where(new Column("height").gt(168))
.where("height > 168")
.show()
//查询姓刘的同志 select * from xxx where name like '刘%'、'刘_'
people.select("name", "age", "height").where("name like '刘%'").show()
//按照年龄进行分组,计算出不同年龄的人数 select age, count(1) from xxx group by age
people.select(new Column("age")).groupBy("age").count().show()
println("=====================SQL操作方式========================")
//注册一张临时表
people.createOrReplaceTempView("people")
val sql =
"""
|select
| age,
| count(*) countz
|from people
|where height < 178.8
|group by age
""".stripMargin
spark.sql(sql).show()
spark.stop()
}
}
3. SparkSQL的编程模型的创建
? SparkSQL中的编程模型主要有DataFrame和DataSet。
3.1. DataFrame的构建
? 构建的方式分为了两类操作,一类是基于JavaBean的反射操作来构建;另一类是动态编程的方式构建。这两种构建方式主要区别就在元数据的识别方式.
3.1.1. 基于反射的方式构建DataFrame
import scala.collection.JavaConversions._
//DataFrame的构建方式
object _01SparkSQLDataFrameOps {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setMaster("local[*]")
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
.registerKryoClasses(Array(classOf[Person]))
.setAppName("SparkSQLDataFrame")
val spark = SparkSession.builder()
.config(conf)
.getOrCreate()
val scalaList = List(
new Person("赵阳", 19, 178.8),
new Person("孙望", 20, 180.0),
new Person("刘梦男", 21, 165.5),
new Person("马静", 18, 168.5)
)
// reflection2DataFrame1(scalaList, spark)
reflection2DataFrame2(scalaList, spark)
spark.stop()
}
def reflection2DataFrame2(scalaList:List[Person], spark:SparkSession): Unit = {
val rdd = spark.sparkContext.parallelize(scalaList)
val pdf = spark.createDataFrame(rdd, classOf[Person])
pdf.printSchema()
pdf.show()
}
def reflection2DataFrame1(scalaList:List[Person], spark:SparkSession): Unit = {
/*
两种方式,进行scala和java的普通集合之间的互相转换
第一种,引入隐式转换import JavaConversions._
第二种,直接调用相关api
eg.JavaConversions.seqAsJavaList(scalaList)
*/
//创建DataFrame
// val list = JavaConversions.seqAsJavaList(scalaList)
val pdf = spark.createDataFrame(scalaList, classOf[Person])
pdf.printSchema()
pdf.show()
}
3.1.2. 基于动态编程的方式构建DataFrame
? 动态编程,主要使用到了两个新的API,一个Row,一个StructType,Row代表的就是二维表中的一行记录,StructType代表的是这张二维表的元数据,主要包含,列名、类型、是否为空。
def dynamic2DataFrame2(scalaList:List[Person], spark:SparkSession): Unit = {
val rowRDD = spark.sparkContext.parallelize(scalaList).map(person => {
Row(person.getName, person.getAge, person.getHeight)
})
val schema = StructType(List(
StructField("name", DataTypes.StringType, false),
StructField("age", DataTypes.IntegerType, false),
StructField("height", DataTypes.DoubleType, true)
))
val pdf = spark.createDataFrame(rowRDD, schema)
pdf.printSchema()
pdf.show()
}
def dynamic2DataFrame1(scalaList:List[Person], spark:SparkSession): Unit = {
val rowList:List[Row] = scalaList.map(person => {
Row(person.getName, person.getAge, person.getHeight)
})
val schema = StructType(List(
StructField("name", DataTypes.StringType, false),
StructField("age", DataTypes.IntegerType, false),
StructField("height", DataTypes.DoubleType, true)
))
val pdf = spark.createDataFrame(rowList, schema)
pdf.printSchema()
pdf.show()
}
def reflection2DataFrame2(scalaList:List[Person], spark:SparkSession): Unit = {
val rdd = spark.sparkContext.parallelize(scalaList)
val pdf = spark.createDataFrame(rdd, classOf[Person])
pdf.printSchema()
pdf.show()
}
总结:
? 在开发过程中使用这两都可以,但是动态编程的方式要比反射的方式更加的灵活,所以建议使用动态编程。
3.2. DataSet的构建
? 使用sparksession.createDataset()构建,示例如下:
val scalaList = List(
new Person("赵阳", 19, 178.8),
new Person("孙望", 20, 180.0),
new Person("刘梦男", 21, 165.5),
new Person("马静", 18, 168.5)
)
val pds:Dataset[Person] = spark.createDataset(scalaList)
? 但是编译没有通过,报错如下:构建Dataset需要一个encoder来进行类型的推断,但是目前仅支持两种类型:primitive type(Int, String, Float等等基本数据类型) 和 case class,其它类型暂时不支持。同时使用上述两种类型的时候,还必须要引入sparksession中的隐式转换,import spark.implicits._,二者缺一不可。
object _02SparkSQLDataSetOps {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setMaster("local[*]")
.setAppName("SparkSQLDataSet")
val spark = SparkSession.builder()
.config(conf)
.getOrCreate()
val scalaList = List(
new Person("赵阳", 19, 178.8),
new Person("孙望", 20, 180.0),
new Person("刘梦男", 21, 165.5),
new Person("马静", 18, 168.5)
)
import spark.implicits._
val stuList = scalaList.map(person => {
Student(person.getName, person.getAge, person.getHeight)
})
val pds:Dataset[Student] = spark.createDataset(stuList)
pds.printSchema()
pds.show()
spark.stop()
}
}
case class Student(name:String, age:Int, height:Double)
3.3. DataFrame、DataSet以及RDD之间的相互转换
/**
* RDD DataFrame Dataset三者之间的互相转化
* 基于上述的操作,我们可以非常灵活的进行SparkCore和SparkSQL之间的编程切换
*/
object _03ApiConvertOps {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setMaster("local[*]")
.setAppName("ApiConvertOps")
val spark = SparkSession.builder()
.config(conf)
.getOrCreate()
val scalaList = List(
Student("赵阳", 19, 178.8),
Student("孙望", 20, 180.0),
Student("刘梦男", 21, 165.5),
Student("马静", 18, 168.5)
)
import spark.implicits._
val stuRDD = spark.sparkContext.parallelize(scalaList)
/*
rdd --> dataframe
rdd转化为dataframe,一种方式就是通过动态编程或者反射,
还有一种更为简单的方式进行转换,使用rdd.toDF,但前提是需要引入spark.implicits._
*/
val sdf = stuRDD.toDF("aaa", "bbb", "ccc")
sdf.show()
/*
rdd --> dataset
一种方式就是通过spark.createDataset(rdd)
还有一种更为简单的方式进行转换,使用rdd.toDS,但前提是需要引入spark.implicits._
另外一点需要说明的是rdd中的类型需要是case class
*/
val sds = stuRDD.toDS()
sds.show()
//dataframe --->rdd ==> dataframe.rdd | dataframe.toJavaRDD
val sdf2RDD = sdf.rdd
sdf2RDD.foreach(row => {
//获取Row 一行中的信息 --->ResultSet
val name = row.getAs[String]("aaa")
val age = row.getInt(1)//row的索引是0开始
val height = row.getAs[Double]("ccc")//建议使用getAs(fieldName)
println(s"name:$name\tage:$age\theight:$height")
})
//dataframe --->dataset(没有办法直接转换)
//dataset ---> rdd ==> dataset.rdd | dataset.javaRDD
sds.rdd.foreach(println)
//dataset ---> dataframe
sds.toDF("bbb", "aha", "heh").show()
spark.stop()
}
}
4. SparkSQL的各种操作
4.1. 创建临时表操作
val scalaList = List(
Student("蔡金廷", 48, 148.8),
Student("范腾文", 19, 180.0),
Student("陈燕瑾", 32, 155.5),
Student("曹煜", 19, 170.5)
)
import spark.implicits._
val sds:Dataset[Student] = spark.createDataset(scalaList)
/**
* 如果一个dataframe或者dataset要想我们进行sql的操作,
* 必须需要将该dataframe或者dataset注册成为一张临时表
*
* createTempView
* createOrReplaceTempView
* createGlobalTempView
* createOrReplaceGlobalTempView
* 是否有replace
* 如果内存中已经存在一张表名相同的表,再去create,会报错
* 是否有global
* 主要就在于当前的表的生命周期周期,
* 没有这global指的是只在当前sparksession中有效,
* 如果有global说明是全局的,可以在当前Application中有效
* 建议使用createOrReplaceTempView
*/
sds.createTempView("student")
// sds.createTempView("student")//TempTableAlreadyExistsException
sds.createOrReplaceGlobalTempView("student")
4.2. sparksql的缓存操作
见如下示意图:
? 和这个内存列存储相关的两个配置参数:
5. SparkSQL数据的加载和落地
5.1. 数据加载
? sparksql中的数据的加载的方式spark.read.load,这都是已经不建议是的了,我们直接使用具体的文件格式的加载方式即可。
/**
* SparkSQL加载数据的各种方式
* sparksql.read.load方式加载的默认文件格式parquet
* parquet是twitter公司为自己的海量数据的存储设计的一种列式文件存储格式,已经贡献给apache基金会成为顶级项目
* 如果要想加载其他的文件格式,只需要指定一个format即可
*/
object _05SparkSQLReadOps {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder()
.master("local[*]")
.appName("SparkSQLRead")
.getOrCreate()
var stuDF = spark.read.format("csv")
.load("file:///E:/data/spark/sql/student.csv")
//toDF 重新构建列名
.toDF("id", "name", "age", "gender", "course", "score")
stuDF.show()
// stuDF = spark.read.csv("file:///E:/data/spark/sql/student.csv")
// stuDF.show()
spark.read.json("file:///E:/data/spark/sql/people.json").show()
spark.read.parquet("file:///E:/data/spark/sql/users.parquet").show()
spark.read.text("file:///E:/data/spark/sql/dailykey.txt").show()
spark.read.orc("file:///E:/data/spark/sql/student.orc").show()
spark.stop()
}
}
5.2. 数据落地
? sparksql中的数据的落地的方式spark.write.save,这都是已经不建议是的了,我们直接使用具体的文件格式的加载方式即可。
/**
* SparkSQL落地数据的各种方式
* spark.write.save(path)
* 如果像一个已经存在的路径中执行保存操作,会报错AnalysisException: already exists.;
* 默认的存储文件格式和读取的默认的文件格式一致,都是parquet。
*
* sparksql中的文件保存的模式SaveMode
* ErrorIfExists:存在即报错,默认的存储格式
* Append:在原有的基础之上追加新的数据
* Overwrite:覆盖(删除原有,重新创建)
* Ignore:忽略(如果有,不执行相关操作,如果没有就执行save操作)
*
*
CREATE TABLE `student` (
`id` INT,
`name` VARCHAR(20),
`age` INT,
`gender` TINYINT,
`course` VARCHAR(20),
`score` FLOAT(3, 1)
) ENGINE=INNODB DEFAULT CHARSET=utf8
*/
object _06SparkSQLSaveOps {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder()
.master("local[*]")
.appName("SparkSQLSave")
.getOrCreate()
var stuDF = spark.read.format("csv")
.load("file:///E:/data/spark/sql/student.csv")
//toDF 重新构建列名
.toDF("id", "name", "age", "gender", "course", "score")
import spark.implicits._
val retDF = stuDF.map(row => {
val id = row.getAs[String]("id").toInt
val name = row.getAs[String]("name")
val age = row.getAs[String]("age").toInt
val gender = row.getAs[String]("gender").toShort
val course = row.getAs[String]("course")
val score = row.getAs[String]("score").toFloat
BigDataStudent(id, name, age, gender, course, score)
})
retDF.printSchema()
//保存数据
// stuDF.write.mode(SaveMode.Ignore)//指定存储模式
// .save("file:///E:/data/out/spark-sql/stu")
// stuDF.write.orc("file:///E:/data/out/spark-sql/stu-orc")
val url = "jdbc:mysql:///test?useUnicode=true&characterEncoding=UTF-8"
val table = "student"
val properties = new Properties()
properties.put("user", "bigdata")
properties.put("password", "sorry")
retDF.write.mode(SaveMode.Append).jdbc(url, table, properties)
spark.stop()
}
}
case class BigDataStudent(
id:Int,
name:String,
age:Int,
gender:Short,
course:String,
score:Float)
6. SparkSQL和Hive的整合
? 这也是sparksql最常见的一种处理方式,也就是加载hive表中的数据,进行业务处理,最后将结果存储到hive或者其他地方。
6.1. 需求:
? 两张基础表数据:teacher_basic和teacher_info,
? 数据结构:
? teacher_basic
列名 | 解释 |
name | 老师姓名 |
age | 老师年龄 |
married | 是否已婚 |
courses | 正在带的科目数量 |
? teacher_info
列名 | 类型 | 解释 |
name | string | 老师姓名 |
height | double | 身高 |
? 要求,基于上述两张表,进行hive的hql操作,首先在hive相应数据库中构建相关的表,并加载数据,其次进行关联操作,查询老师的所有信息,将结果存储到teacher表中。
create table teacher as
select
b.name,
b.age,
b.married,
b.courses,
i.height
from teacher_basic b
join teacher_info i on b.name = i.name
? 现在需要使用sparksql的方式进行处理。
6.2. 业务实现
/**
* SparkSql和hive的整合操作
*
* teacher_basic -->name老师姓名 age老师年龄 married是否已婚 courses正在带的科目数量
* teacher_info --->namestring老师姓名 heightdouble身高
*
* teacher
*
* 注意:SparkSession.sql操作每次只能执行一条sql语句,不可以执行多条sql操作,因为只有一个返回值DataFrame。
*/
object _01SparkSqlIntergationWithHiveOps {
def main(args: Array[String]): Unit = {
if(args == null || args.length <2) {
println(
"""
|Parameter Errors! Usage: <teacher_basic> <teacher_info>
""".stripMargin)
System.exit(-1)
}
val Array(basicPath, infoPath) = args
val spark = SparkSession.builder()
.appName("SparkSqlIntergationWithHive")
// .master("local[*]")
.enableHiveSupport()//支持hive中特定的方言操作
.getOrCreate()
//首先创建数据库
var sql =
"""
|create database if not exists `test_1901`
""".stripMargin
spark.sql(sql)
//创建teacher_basic
sql =
"""
|create table if not exists `test_1901`.`teacher_basic` (
| name string,
| age int,
| married boolean,
| courses int
|) row format delimited
|fields terminated by ','
""".stripMargin
spark.sql(sql)
//创建teacher_info
sql =
"""
|create table if not exists `test_1901`.`teacher_info` (
| name string,
| height double
|) row format delimited
|fields terminated by ','
""".stripMargin
spark.sql(sql)
//加载数据teacher_basic.txt和teacher_info.txt
val loadBasic = s"load data inpath '${basicPath}' into table `test_1901`.`teacher_basic`"
spark.sql(loadBasic)
val loadInfo = s"load data inpath '${infoPath}' into table `test_1901`.`teacher_info`"
spark.sql(loadInfo)
//执行关联查询
val joinSQL =
"""
|select
| b.name,
| b.age,
| b.married,
| b.courses,
| i.height
|from `test_1901`.`teacher_basic` b
|join `test_1901`.`teacher_info` i on b.name = i.name
|where b.courses > 0
""".stripMargin
val teacher = spark.sql(joinSQL)
//存储结果
teacher.write.saveAsTable("`test_1901`.`teacher`")
spark.stop()
}
}
6.3. 整合注意的地方
- 需要将hive的配置文件hive-site.xml添加进行spark程序的classpath中
通常我们将hive-site.xml放入$SPARK_HOME/conf - 需要将mysql的驱动程序也要添加进spark的classpath中
通常将mysql.jar放入$SPARK_HOME/jars中,或者将jar打到spark程序中 - 为了保险起见,大家可以将hive-site.xml打入程序的classpath中,放入resources目录下即可。