本书及作者信息
作者: 余辉
微信公众号:辉哥大数据
备注:本书配套示例源码、PPT课件、教学视频与作者答疑服务,每周更新一部分,敬请期待…
红包奖励:找出本书问题,评论区并指正,且留下联系方式
本书其他章节
- Spark大数据开发与应用案例(视频教学版)(一)–文前
- Spark大数据开发与应用案例(视频教学版)(二)–第一章上
- Spark大数据开发与应用案例(视频教学版)(三)–第一章下
- Spark大数据开发与应用案例(视频教学版)(四)–第二章上
- Spark大数据开发与应用案例(视频教学版)(五)–第二章下
- Spark大数据开发与应用案例(视频教学版)(六)–第三章上
- Spark大数据开发与应用案例(视频教学版)(七)–第三章下
- Spark大数据开发与应用案例(视频教学版)(八)–第四章上
- Spark大数据开发与应用案例(视频教学版)(九)–第四章下
- Spark大数据开发与应用案例(视频教学版)(十)–第五章
- Spark大数据开发与应用案例(视频教学版)(十一)–第六章
- Spark大数据开发与应用案例(视频教学版)(十二)–第七章
- Spark大数据开发与应用案例(视频教学版)(十三)–第八章
- Spark大数据开发与应用案例(视频教学版)(十四)–第九章
第10章 Spark SQL抽象编程详解
本章将引领你深入探索DataFrame和DataSet 的奥秘,从创建到输出,再到各类运算,全面展现其数据处理能力。同时,你还将学习如何在RDD与SQL代码间自由切换,掌握RDD、DataSet、DataFrame间的互转技巧。本章是你掌握DataFrame编程的必备指南。
本章主要知识点:
- DataFrame创建
- DataFrame运算
- DataFrame输出
- RDD代码和SQL代码混合编程
- RDD/DS/DF互转
10.1 DataFrame创建
在Spark SQL中SparkSession是创建DataFrames和执行SQL的入口。创建DataFrames有三种方式:
- 从一个已存在的RDD进行转换。
- 从JSON/Parquet/CSV/ORC等结构化文件源创建。
- 从Hive/JDBC各种外部结构化数据源(服务)创建。
创建DataFrame,需要创建“RDD + 元信息schema定义”。RDD来自于数据,Schema则可以由开发人员定义,或者由框架从数据中推断。下文将通过具体案例带领读者进入实战练习环节。
10.1.1 使用RDD创建DataFrame
- 将RDD关联case class创建DataFrame。示例代码如下:
代码10-1 DataFrameDemo1.scala
object DataFrameDemo1 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setAppName(this.getClass.getSimpleName)
.setMaster("local[*]")
//1.SparkSession,是对SparkContext的增强
val session: SparkSession = SparkSession.builder()
.config(conf)
.getOrCreate()
//2.创建DataFrame
//2.1先创建RDD
val lines: RDD[String] = session.sparkContext.textFile("data/user.txt")
//2.2对数据进行整理并关联Schema
val tfBoy: RDD[Boy] = lines.map(line => {
val fields = line.split(",")
val name = fields(0)
val age = fields(1).toInt
val fv = fields(2).toDouble
Boy(name, age, fv) //字段名称,字段的类型
})
//2.3将RDD关联schema,将RDD转成DataFrame
//导入隐式转换
import session.implicits._
val df: DataFrame = tfBoy.toDF
//打印DataFrame的Schema信息
df.printSchema()
//3.将DataFrame注册成视图(虚拟的表)
df.createTempView("v_users")
//4.写sql(Transformation)
val df2: DataFrame = session.sql("select * from v_users order by fv desc, age asc")
//5.触发Action
df2.show()
//6.释放资源
session.stop()
}
}
case class Boy(name: String, age: Int, fv: Double)
执行结果如图10-1所示。
2. 将RDD关联scala class创建DataFrame,示例代码如下:
代码10-2 DataFrameDemo2.scala
package chapter10
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{DataFrame, SparkSession}
import scala.beans.BeanProperty
object DataFrameDemo2 {
def main(args: Array[String]): Unit = {
//1.创建SparkSession
val spark = SparkSession.builder()
.appName("DataFrameDemo2")
.master("local[*]")
.getOrCreate()
//2.创建RDD
val lines: RDD[String] = spark.sparkContext.textFile("BookData/input/10boy.txt")
//2将数据封装到普通的class中
val boyRDD: RDD[Boy2] = lines.map(line => {
val fields = line.split(",")
val name = fields(0)
val age = fields(1).toInt
val fv = fields(2).toDouble
new Boy2(name, age, fv) //字段名称,字段的类型
})
//3.将RDD和Schema进行关联
val df = spark.createDataFrame(boyRDD, classOf[Boy2])
//df.printSchema()
//4.使用DSL风格的API
import spark.implicits._
df.show()
spark.stop()
}
}
//参数前面必须有var或val
//必须添加给字段添加对应的getter方法,在scala中,可以@BeanProperty注解
class Boy2(
@BeanProperty
val name: String,
@BeanProperty
val age: Int,
@BeanProperty
val fv: Double) {
}
执行结果如图10-2所示。注意,普通的Scala class必须在成员变量加上@BeanProperty属性,因为Spark SQL需要通过反射调用getter获取schema信息。
3. 将RDD关联java class创建DataFrame,示例代码如下:
代码10-3 JBoy的javabean的JBoy .scala
package chapter10;
public class JBoy {
private String name;
private Integer age;
private Double fv;
public String getName() {
return name;
}
public Integer getAge() {
return age;
}
public Double getFv() {
return fv;
}
public JBoy(String name, Integer age, Double fv) {
this.name = name;
this.age = age;
this.fv = fv;
}
}
代码10-4 主函数DataFrameDemo3.scala
package chapter10
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{DataFrame, SparkSession}
object DataFrameDemo3 {
def main(args: Array[String]): Unit = {
val spark: SparkSession = SparkSession.builder()
.appName(this.getClass.getSimpleName)
.master("local[*]")
.getOrCreate()
val lines = spark.sparkContext.textFile("BookData/input/10boy.txt")
//将RDD关联的数据封装到Java的class中,但是依然是RDD
val jboyRDD: RDD[JBoy] = lines.map(line => {
val fields = line.split(",")
new JBoy(fields(0), fields(1).toInt, fields(2).toDouble)
})
//强制将关联了schema信息的RDD转成DataFrame
val df: DataFrame = spark.createDataFrame(jboyRDD, classOf[JBoy])
df.printSchema()
//注册视图
df.createTempView("v_boy")
//写sql
val df2: DataFrame = spark.sql("select name, age, fv from v_boy order by fv desc, age asc")
df2.show()
spark.stop()
}
}
执行结果如图10-3所示。
4. 将RDD关联Schema创建DataFrame,示例代码如下:
代码10-4 DataFrameDemo4.scala
package chapter10
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.types.{DoubleType, IntegerType, StringType, StructField, StructType}
import org.apache.spark.sql.{DataFrame, Row, SparkSession}
object DataFrameDemo4 {
def main(args: Array[String]): Unit = {
val spark: SparkSession = SparkSession.builder()
.appName(this.getClass.getSimpleName)
.master("local[*]")
.getOrCreate()
val lines = spark.sparkContext.textFile("BookData/input/10boy.txt")
//将RDD关联了Schema,但是依然是RDD
val rowRDD: RDD[Row] = lines.map(line => {
val fields = line.split(",")
Row(fields(0), fields(1).toInt, fields(2).toDouble)
})
val schema = StructType.apply(
List(
StructField("name", StringType),
StructField("age", IntegerType),
StructField("fv", DoubleType)
)
)
val df: DataFrame = spark.createDataFrame(rowRDD, schema)
//打印schema信息
df.printSchema()
//注册视图
df.createTempView("v_boy")
//写sql
val df2: DataFrame = spark.sql("select name, age, fv from v_boy order by fv desc, age asc")
df2.show()
spark.stop()
}
}
执行结果如图10-4所示。
10.1.2 从结构化文件创建DataFrame
- 从csv文件(不带header)中创建
文件名称为10stu.csv,文件内容如下:
1,张飞,21,北京,80.0
2,关羽,23,北京,82.0
3,赵云,20,上海,88.6
4,刘备,26,上海,83.0
5,曹操,30,深圳,90.0
从csv文件(不带header)进行创建,示例代码如下:
代码10-5 DataFrameDemo5.scala
package chapter10
import org.apache.spark.sql.SparkSession
object DataFrameDemo5 {
def main(args: Array[String]): Unit = {
val spark: SparkSession = SparkSession.builder()
.appName(this.getClass.getSimpleName)
.master("local[*]")
.getOrCreate()
val df = spark.read.csv("BookData/input/10stu.csv")
df.printSchema()
df.show()
}
}
执行结果如图10-5所示。
可以上图可以看出,框架把读取进来的csv数据自动生成的schema,其字段名为_c0,_c1,_c2,_c3,_c4。字段类型全为String。但这种数据格式不一定符合我们的需求。下面我们使用自定义Schema创建DataFrame。
2. 从csv文件(不带header)自定义Schema进行创建
创建DataFrame时,传入自定义的schema。schema在API中用StructType这个类来描述,字段用StructField来描述。示例代码如下:
代码10-6 DataFrameDemo6.scala
package chapter10
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.types.{DataTypes, StructType}
object DataFrameDemo6 {
def main(args: Array[String]): Unit = {
val spark: SparkSession = SparkSession.builder()
.appName(this.getClass.getSimpleName)
.master("local[*]")
.getOrCreate()
val schema = new StructType()
.add("id", DataTypes.IntegerType)
.add("name", DataTypes.StringType)
.add("age", DataTypes.IntegerType)
.add("city", DataTypes.StringType)
.add("score", DataTypes.DoubleType)
val df = spark.read.schema(schema).csv("BookData/input/10stu.csv")
df.printSchema()
df.show()
}
}
执行结果如图10-6所示。
3. 从csv文件(带header)进行创建
文件名称10stu2.csv,文件内容如下:
id,name,age,city,score
1,张飞,21,北京,80.0
2,关羽,23,北京,82.0
3,赵云,20,上海,88.6
4,刘备,26,上海,83.0
5,曹操,30,深圳,90.0
示例代码如下:
代码10-7 DataFrameDemo7.scala
package chapter10
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.types.{DataTypes, StructType}
object DataFrameDemo7 {
def main(args: Array[String]): Unit = {
val spark: SparkSession = SparkSession.builder()
.appName(this.getClass.getSimpleName)
.master("local[*]")
.getOrCreate()
val df = spark.read
.option("header",true) //读取表头信息
.csv("BookData/input/10stu2.csv")
df.printSchema()
df.show()
}
}
执行结果如图10-7所示。
注意,此文件的第一行是字段描述信息,需要特别处理,否则会被当做RDD中的一行数据。这个处理的关键点是设置一个header=true的参数,代码示例如下:
val df = spark.read
.option("header",true) //读取表头信息
.csv("data_ware/demodata/stu.csv")
df.printSchema()
df.show()
注意,虽然字段名正确指定,但是字段类型还是无法确定,默认情况下全视作String对待,当然,可以开启一个参数inferSchema=true来让框架对csv中的数据字段进行合理的类型推断。
val df = spark.read
.option("header",true)
.option("inferSchema",true) //推断字段类型
.csv("data_ware/demodata/stu.csv")
df.printSchema()
df.show()
注意,如果推断的结果不如人意,当然可以指定自定义schema。让框架自动推断schema效率低,不建议使用!
4. 从JSON文件进行创建
文件名称10student.json,文件内容如下:
{"name":"A","lesson":"Math","score":100}
{"name":"B","lesson":"Math","score":100}
{"name":"C","lesson":"Math","score":99}
{"name":"D","lesson":"Math","score":98}
{"name":"A","lesson":"E","score":100}
{"name":"B","lesson":"E","score":99}
{"name":"C","lesson":"E","score":99}
{"name":"D","lesson":"E","score":98}
示例代码10-8如下:
代码10-8 DataFrameDemo8.scala
package chapter10
import org.apache.spark.sql.SparkSession
object DataFrameDemo8 {
def main(args: Array[String]): Unit = {
val spark: SparkSession = SparkSession.builder()
.appName(this.getClass.getSimpleName)
.master("local[*]")
.getOrCreate()
val df = spark.read.json("BookData/input/10student.json")
df.printSchema()
df.show()
}
}
执行结果如图10-8所示。
5. 从Parquet文件进行创建
Parquet文件是一种列式存储文件格式,文件自带schema描述信息,任意拿一个DataFrame,调用write.parquet()方法即可将df保存为一个parquet文件。代码示例:
val df = spark.read.parquet("data/parquet/")
- 从orc文件进行创建
ORC文件是一种列式存储文件格式,文件自带schema描述信息,任意拿一个DataFrame,调用.write.format(“orc”).save(“/path”)方法即可将df保存为一个ORC文件。代码示例:
val df = spark.read.orc("data/orcfiles/")
10.1.3 外部存储服务创建DF
从JDBC连接数据库服务器进行创建。
1. 实验数据准备
在一个MySQL服务器中,创建一个数据库spark,在库中再创建一个表student,如图10-9所示。
要使用JDBC连接读取数据库的数据,需要引入JDBC的驱动jar包依赖:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
代码示例:
val props = new Properties()
props.setProperty("user","root")
props.setProperty("password","yuhui888")
val df = spark.read.jdbc("jdbc:mysql://yuhui01:3306/spark","student",props)
df.show()
执行结果如下:
+---+------+---+------+-----+
| id|name |age|city |score|
+---+------+---+------+-----+
| 1| 张飞| 21| 北京| 80.0|
| 2| 关羽| 23| 北京| 82.0|
| 3| 赵云| 20| 上海| 88.6|
| 4| 刘备| 26| 上海| 83.0|
| 5| 曹操| 30| 深圳| 90.0|
+---+------+---+------+-----+
Spark SQL添加了spark-hive的依赖,并在sparkSession构造时开启enableHiveSupport,之后就整合了Hive的功能(通俗地说,就是Spark SQL具备了Hive的功能),spark-hive的依赖和依赖中的组件如图10-10所示。
既然具备了Hive的功能,那么就可以执行Hive中所有能执行的动作,包括建表、show表、建库、show库、alter表,等等。只不过,此时看见的表是Spark中集成的Hive的本地元数据库中的表。
如果想让Spark中集成的Hive看见你外部集群中的Hive表,只要修改配置:把Spark端的Hive的元数据服务地址指向外部集群中Hive的元数据服务地址。有两种指定办法:
在Spark端加入hive-site.xml,里面配置目标元数据库MySQL的连接信息,这会使得Spark中集成的Hive直接访问MySQL元数据库。hive-site.xml配置如下:
<configuration>
<!-- 使用JDBC连接到MySQL数据库,自动创建数据库(如果不存在) -->
<property>
<name>javax.jdo.option.ConnectionURL</name> <value>jdbc:mysql://<mysql-host>:<mysql-port>/<hive-metastore-db>?createDatabaseIfNotExist=true</value>
</property>
<!-- 配置MySQL数据库JDBC连接驱动 -->
<property>
<name>javax.jdo.option.ConnectionDriverName</name>
<value>com.mysql.jdbc.Driver</value>
</property>
<!-- 配置连接数据库的用户名 -->
<property>
<name>javax.jdo.option.ConnectionUserName</name>
<value><mysql-username></value>
</property>
<!-- 配置连接数据库的密码 -->
<property>
<name>javax.jdo.option.ConnectionPassword</name>
<value><mysql-password></value>
</property>
<!-- 注释:以下可能包含其他Hive相关的配置信息,具体配置根据需求添加 -->
</configuration>
在Spark端加入hive-site.xml,里面配置目标Hive的元数据服务器地址,这会使得Spark中集成的Hive通过外部独立的Hive元数据服务来访问元数据库。hive-site.xml配置如下:
<configuration>
<!-- HIVE元数据配置 -->
<property>
<name>hive.metastore.uris</name>
<value>thrift://<metastore-host>:<metastore-port></value>
</property>
<!-- 其他Hive配置 -->
</configuration>
- 从Hive创建DataFrame
Spark SQL通过spark-hive整合包来集成Hive的功能。Spark SQL加载“外部独立Hive”的数据,本质上是不需要外部独立Hive参与的,因为外部独立Hive的表数据就在HDFS中,元数据信息在MySQL中不管数据还是元数据,Spark SQL都可以直接去获取!
从Hive创建DataFrame的操作步骤如下:
要在工程中添加spark-hive的依赖jar以及MySQL的JDBC驱动jar。
<!-- MySQL的依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<!-- Spark整合Hive的依赖,即可以读取Hive的源数据库,使用Hive特点的SQL-->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-hive_2.13</artifactId>
<version>3.5.3</version>
</dependency>
要在工程中添加hive-site.xml、core-site.xml和hdfs-site.xml配置文件。
在hive-site.xml中配置Hive元数据服务地址信息。
<configuration>
<property>
<name>hive.metastore.uris</name>
<value>thrift://yuhui01:9083</value>
</property>
</configuration>
创建sparksession时需要调用.enableHiveSupport()方法。
val spark = SparkSession
.builder()
.appName(this.getClass.getSimpleName)
.master("local[*]")
// 启用hive支持,需要调用enableHiveSupport,还需要添加一个依赖 spark-hive
// 默认sparksql内置了自己的hive
// 如果程序能从classpath中加载到hive-site配置文件,那么它访问的hive元数据库就不是本地内置的了,而是配置中所指定的元数据库了
// 如果程序能从classpath中加载到core-site配置文件,那么它访问的文件系统也不再是本地文件系统了,而是配置中所指定的hdfs文件系统了
.enableHiveSupport()
.getOrCreate()
加载Hive中的表。
val df = spark.sql("select * from t1")
注意,如果你也用DataFrame注册了一个同名的视图,那么这个视图名会替换掉Hive的表。
3. 从HBase创建DataFrame
Spark SQL可以连接任意外部数据源(只要有对应的“连接器”即可)Spark SQL对HBase是有第三方连接器(华为)的,但是久不维护。建议用Hive作为连接器(Hive可以访问HBase,而Spark SQL可以集成Hive)在HBase中建表:
create 'bigdata_stu','f'
插入数据到HBase表bigdata_stu:
put 'bigdata_stu','001','f:name','zhangsan'
put 'bigdata_stu','001','f:name','张三'
put 'bigdata_stu','001','f:age','26'
put 'bigdata_stu','001','f:gender','m'
put 'bigdata_stu','001','f:salary','28000'
put 'bigdata_stu','002','f:name','lisi'
put 'bigdata_stu','002','f:age','22'
put 'bigdata_stu','002','f:gender','m'
put 'bigdata_stu','002','f:salary','26000'
put 'bigdata_stu','003','f:name','wangwu'
put 'bigdata_stu','003','f:age','21'
put 'bigdata_stu','003','f:gender','f'
put 'bigdata_stu','003','f:salary','24000'
put 'bigdata_stu','004','f:name','zhaoliu'
put 'bigdata_stu','004','f:age','22'
put 'bigdata_stu','004','f:gender','f'
put 'bigdata_stu','004','f:salary','25000'
创建Hive外部表映射HBase中的表:
CREATE EXTERNAL TABLE bigdata_stu
(
id string ,
name string ,
age int ,
gender string ,
salary double
)
STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
WITH SERDEPROPERTIES ( 'hbase.columns.mapping'=':key,f:name,f:age,f:gender,f:salary')
TBLPROPERTIES ( 'hbase.table.name'='default:bigdata_stu')
;
工程中放置hbase-site.xml配置文件:
<configuration>
<!-- 配置HBase的根目录 -->
<property>
<name>hbase.rootdir</name>
<value>hdfs://yuhui01:8020/hbase</value>
</property>
<!-- 配置HBase是否为分布式集群模式 -->
<property>
<name>hbase.cluster.distributed</name>
<value>true</value>
</property>
<!-- 配置HBase使用的ZooKeeper集群地址 -->
<property>
<name>hbase.zookeeper.quorum</name>
<value>yuhui01:2181,yuhui02:2181,yuhui03:2181</value>
</property>
<!-- 配置HBase是否强制检查文件流的能力 -->
<property>
<name>hbase.unsafe.stream.capability.enforce</name>
<value>false</value>
</property>
</configuration>
工程中添加hive-hbase-handler连接器依赖:
<dependency>
<groupId>org.apache.hive</groupId>
<artifactId>hive-hbase-handler</artifactId>
<version>2.3.7</version>
<exclusions>
<exclusion>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
</exclusion>
</exclusions>
</dependency>
以读取Hive表的方式直接读取即可:
spark.sql("select * from bigdata_stu")
10.2 DataFrame运算
本节将通过Spark SQL的两种风格对DataFrame进行运算,分别是SQL操作和DSL风格。
10.2.1 SQL风格操作
将DataFrame 注册为一个临时视图view,然后就可以针对view直接执行各种SQL。临时视图有两种:Session级别视图和Global级别视图。
Session级别视图在Session范围内有效,Session退出后,表就失效了。
Global全局视图则在application级别有效。
注意,使用全局表时需要全路径访问global_temp.people。
// application全局有效
df.createGlobalTempView("stu")
spark.sql(
"""
|select * from global_temp.stu a order by a.score desc
""".stripMargin)
.show()
// session有效
df.createTempView("s")
spark.sql(
"""
|select * from s order by score
""".stripMargin)
.show()
val spark2 = spark.newSession()
// 全局有效的view可以在session2中访问
spark2.sql("select id,name from global_temp.stu").show()
// session有效的view不能在session2中访问
spark2.sql("select id,name from s").show()
10.2.2 DSL风格API(TableApi)语法
在Apache Spark中,DSL(Domain-Specific Language)风格的API主要指的是Spark SQL和DataFrame API,它们提供了一种类似于SQL的表达式语言来操作数据。这些API使得数据处理更加直观和易于理解,尤其是对于熟悉SQL的数据科学家和分析师来说。
- 数据准备
val df = spark.read
.option("header", true)
.option("inferSchema", true)
.csv("data_ware/demodata/stu.csv")
- 基本select及表达式
/**
* 逐行运算
*/
// 使用字符串表达"列"
df.select("id","name").show()
// 如果要用字符串形式表达sql表达式,应该使用selectExpr方法
df.selectExpr("id+1","upper(name)").show
// select方法中使用字符串sql表达式,会被视作一个列名从而出错
// df.select("id+1","upper(name)").show()
import spark.implicits._
// 使用$符号创建Column对象来表达"列"
df.select($"id",$"name").show()
// 使用单边单引号创建Column对象来表达"列"
df.select('id,'name).show()
// 使用col函数来创建Column对象来表达"列"
import org.apache.spark.sql.functions._
df.select(col("id"),col("name")).show()
// 使用Dataframe的apply方法创建Column对象来表达列
df.select(df("id"),df("name")).show()
// 对Column对象直接调用Column的方法,或调用能生成Column对象的functions来实现sql中的运算表达式
df.select('id.plus(2).leq("4").as("id2"),upper('name)).show()
df.select('id+2 <= 4 as "id2",upper('name)).show()
- 字段重命名
/**
* 字段重命名
*/
// 对column对象调用as方法
df.select('id as "id2",$"name".as("n2"),col("age") as "age2").show()
// 在selectExpr中直接写sql的重命名语法
df.selectExpr("cast(id as string) as id2","name","city").show()
// 对dataframe调用withColumnRenamed方法对指定字段重命名
df.select("id","name","age").withColumnRenamed("id","id2").show()
// 对dataframe调用toDF对整个字段名全部重设
df.toDF("id2","name","age","city2","score").show()
- 条件过滤
/**
* 逐行过滤
*/
df.where("id>4 and score>95")
df.where('id > 4 and 'score > 95).select("id","name","age").show()
- 分组聚合
/**
* 分组聚合
*/
df.groupBy("city").count().show()
df.groupBy("city").min("score").show()
df.groupBy("city").max("score").show()
df.groupBy("city").sum("score").show()
df.groupBy("city").avg("score").show()
df.groupBy("city").agg(("score","max"),("score","sum")).show()
df.groupBy("city").agg("score"->"max","score"->"sum").show()
- 子查询
/**
* 子查询相当于:
* select
* *
* from
* (
* select
* city,sum(score) as score
* from stu
* group by city
* ) o
* where score>165
*/
df.groupBy("city")
.agg(sum("score") as "score")
.where("score > 165")
.select("city", "score")
.show()
- Join关联查询
/**
* 总结:
* join方式: joinType: String
* join条件:
* 可以直接传join列名:usingColumn/usingColumns : Seq(String) 注意:右表的join列数据不会出现结果中
* 可以传join自定义表达式: Column.+(1) === Column df1("id")+1 === df2("id")
*/
// 笛卡儿积
df1.crossJoin(df2).show()
// 给join传入一个连接条件; 这种方式,要求,你的join条件字段在两个表中都存在且同名
df1.join(df2,"id").show()
// 传入多个join条件列,要求两表中这多个条件列都存在且同名
df1.join(df2,Seq("id","sex")).show()
// 传入一个自定义的连接条件表达式
df1.join(df2,df1("id") + 1 === df2("id")).show()
// 还可以传入join方式类型: inner(默认), left ,right, full ,left_semi, left_anti
df1.join(df2,df1("id")+1 === df2("id"),"left").show()
df1.join(df2,Seq("id"), "right").show()
- Union操作
在Spark SQL中,UNION 和 UNION ALL 都要求合并的查询结果具有相同的字段个数、字段名称和字段类型,其中 UNION ALL 保留所有行而 UNION 去除重复行。
df1.union(df2).show()
- 窗口分析函数调用
文件名称10stu2.csv,文件内容如下:
id,name,age,sex,city,score
1,张飞,21,M,北京,80
2,关羽,23,M,北京,82
7,周瑜,24,M,北京,85
3,赵云,20,F,上海,88
4,刘备,26,M,上海,83
8,孙权,26,M,上海,78
5,曹操,30,F,深圳,90.8
6,孔明,35,F,深圳,77.8
9,吕布,28,M,深圳,98
求每个城市中成绩最高的两个人的信息,使用SQL编写:
select
id,name,age,sex,city,score
from
(
select
id,name,age,sex,city,score,
row_number() over(partition by city order by score desc) as rn
from t
) o
where rn<=2
再使用DSL风格的API实现,示例代码如下:
代码10-9 DML_DSLAPI_WINDOW.scala
package chapter10
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.expressions.Window
/**
* author: yuhui
* descriptions: 用dsl风格api实现sql中的窗口分析函数
* date: 2024 - 12 - 02 4:39 下午
*/
object DML_DSLAPI_WINDOW {
def main(args: Array[String]): Unit = {
val spark: SparkSession = SparkSession.builder()
.appName(this.getClass.getSimpleName)
.master("local[*]")
.getOrCreate()
val df = spark.read.option("header", true).csv("BookData/input/10stu2.csv")
import spark.implicits._
import org.apache.spark.sql.functions._
val window = Window.partitionBy(Symbol("city")).orderBy(Symbol("score").desc)
df.select(Symbol("id"), Symbol("name"), Symbol("age"), Symbol("sex"), Symbol("city"), Symbol("score"), row_number().over(window) as "rn")
.where(Symbol("rn") <= 2)
.drop("rn") // 最后结果中不需要rn列,可以drop掉这个列
.select("id", "name", "age", "sex", "city", "score") // 或者用select指定你所需要的列
.show()
spark.close()
}
}
执行结果如图10-12所示。
Dataset提供与RDD类似的编程算子,即map、flatMap、reduceByKey等,不过本方式使用比较少见:
如果方便用SQL表达的逻辑,首选SQL。
如果不方便用SQL表达,则可以把Dataset转成RDD后使用RDD的算子。
直接在Dataset上调用类似RDD风格算子的代码示例如下:
/**
* 四、 dataset/dataframe 调 RDD算子
*
* dataset调rdd算子,返回的还是dataset[U], 不过需要对应的 Encoder[U]
*
*/
val ds4: Dataset[(Int, String, Int)] = ds2.map(p => (p.id, p.name, p.age + 10)) // 元组有隐式Encoder自动传入
val ds5: Dataset[JavaPerson] = ds2.map(p => new JavaPerson(p.id,p.name,p.age*2))(Encoders.bean(classOf[JavaPerson])) // Java类没有自动隐式Encoder,需要手动传
val ds6: Dataset[Map[String, String]] = ds2.map(per => Map("name" -> per.name, "id" -> (per.id+""), "age" -> (per.age+"")))
ds6.printSchema()
/**
* root
|-- value: map (nullable = true)
| |-- key: string
| |-- value: string (valueContainsNull = true)
*/
// 从ds6中查询每个人的姓名
ds6.selectExpr("value['name']")
// dataframe上调RDD算子,就等价于 dataset[Row]上调rdd算子
val ds7: Dataset[(Int, String, Int)] = frame.map(row=>{
val id: Int = row.getInt(0)
val name: String = row.getAs[String]("name")
val age: Int = row.getAs[Int]("age")
(id,name,age)
})
// 利用模式匹配从row中抽取字段数据
val ds8: Dataset[Per] = frame.map({
case Row(id:Int,name:String,age:Int) => Per(id,name,age*10)
})
10.3 DataFrame输出
DataFrame在Spark中支持多种输出方式:可直接输出到控制台进行快速查看,也能保存为文件以便持久化存储。此外,它还能将数据写入RDBMS和Hive,并支持在输出时进行分区操作以优化性能。
10.3.1 输出控制台
df.show()
df.show(10) //输出10行
df.show(10,false) // 不要截断列
10.3.2 输出文件
object SaveDF {
def main(args: Array[String]): Unit = {
val spark = SparkUtil.getSpark()
val df = spark.read.option("header",true).csv("data/stu2.csv")
val res = df.where("id>3").select("id","name")
// 展示结果
res.show(10,false)
}
保存结果为文件,格式包括parquet、json、csv、orc、text。文本文件是自由格式,框架无法判断该输出什么样的形式。示例如下:
res.write.parquet("out/parquetfile/")
res.write.csv("out/csvfile")
res.write.orc("out/orcfile")
res.write.json("out/jsonfile")
要将df输出为普通文本文件,则需要将df变成一个列:
res.selectExpr("concat_ws('\001',id,name)")
.write.text("out/textfile")
10.3.3 输出到RDBMS
将dataframe写入MySQL的表:
// 将dataframe通过jdbc写入mysql
val props = new Properties()
props.setProperty("user","root")
props.setProperty("password","yuhui888")
// 可以通过SaveMode来控制写入模式:
aveMode.Append/Ignore/Overwrite/ErrorIfExists(默认)
res.write.mode(SaveMode.Append).jdbc("jdbc:mysql://yuhui01:3306/spark?characterEncoding=utf8","res",props)
10.3.4 输出到Hive
开启Spark的Hive支持,代码如下:
val spark = SparkSession
.builder()
.appName("")
.master("local[*]")
.enableHiveSupport()
.getOrCreate()
需要在工程中添加hive-site.xml、core-site.xml、hdfs-site.xml配置文件,如图10-13所示。
编写代码如下:
// 将dataframe写入Hive,saveAsTable就是保存为Hive的表
// 前提,Spark要开启hiveSupport支持,spark-hive的依赖,Hive的配置文件
res.write.saveAsTable("res")
10.3.5 DF输出时的分区操作
文件名称为04-dept-area7.txt,数据如下:
10,武汉市,hubei,2024-07-01
20,咸宁市,hubei,2024-07-01
30,赤壁市,hubei,2024-07-01
40,赤马港,hubei,2024-07-01
文件名称为04-dept-area8.txt,数据如下:
10,海淀区,beijing,2024-08-01
20,朝阳区,beijing,2024-08-01
30,东城区,beijing,2024-08-01
40,西城区,beijing,2024-08-01
文件名称为04-dept-area9.txt,数据如下:
10,宝安区,shenzhen,2024-09-01
20,南山区,shenzhen,2024-09-01
30,福田区,shenzhen,2024-09-01
40,龙岗区,shenzhen,2024-09-01
将数据上传到HDFS上的/hive_data路径下面,通过Hive进行加载。Hive命令如下:
create table tx(id int,name string) partitioned by (city string);
load data inpath '/hive_data/04-dept-area7.txt' into table tx partition(city="hubei");
load data inpath '/hive_data/04-dept-area8.txt' into table tx partition(city="beijing");
load data inpath '/hive_data/04-dept-area9.txt' into table tx partition(city="shenzheng");
Hive的表tx的目录结构如下:
/user/hive/warehouse/tx/
city=hubei/04-dept-area7.txt
city=beijing/04-dept-area8.txt
city=shenzheng/04-dept-area9.txt
查询的时候,分区标识字段,可以看做表的一个字段来用:
select * from tx where city='hubei'
Hive的表是建立HDFS上面,Hive分区表在HDFS的展示图,如图10-14所示。
通过Spark SQL查询Hive的数据,Spark SQL命令如下,展示结果如图10-15。
/**
* sparksql对分区机制的支持
* 识别已存在分区结构 /tx/city=hubei; /tx/city=beijing; /tx/city=shenzheng;
* 会将所有子目录都理解为数据内容,会将子目录中的city理解为一个字段
*/
spark.read.csv("/user/hive/warehouse/tx").show()
通过Spark SQL能将数据按分区机制输出:
/**
* sparksql对分区机制的支持,将dataframe存储为分区结构
*/
val dfp = spark.read.option("header",true).csv("/user/hive/warehouse/tx")
dfp.write.partitionBy("city").csv("/user/hive/warehouse/tx1")
输入结构如图10-16所示。
本书其他章节
- Spark大数据开发与应用案例(视频教学版)(一)–文前
- Spark大数据开发与应用案例(视频教学版)(二)–第一章上
- Spark大数据开发与应用案例(视频教学版)(三)–第一章下
- Spark大数据开发与应用案例(视频教学版)(四)–第二章上
- Spark大数据开发与应用案例(视频教学版)(五)–第二章下
- Spark大数据开发与应用案例(视频教学版)(六)–第三章上
- Spark大数据开发与应用案例(视频教学版)(七)–第三章下
- Spark大数据开发与应用案例(视频教学版)(八)–第四章上
- Spark大数据开发与应用案例(视频教学版)(九)–第四章下
- Spark大数据开发与应用案例(视频教学版)(十)–第五章
- Spark大数据开发与应用案例(视频教学版)(十一)–第六章
- Spark大数据开发与应用案例(视频教学版)(十二)–第七章
- Spark大数据开发与应用案例(视频教学版)(十三)–第八章
- Spark大数据开发与应用案例(视频教学版)(十四)–第九章