spark sql概述
Spark SQL 是 Spark 用于结构化数据(structured data)处理的 Spark 模块。
DataFrame&DataSet
DataFrame、DataSet是spark SQL中新的数据抽象,建立在RDD的基础上,所以它们也是分布式数据集。
DataFrame
DataFrame 是为数据提供了 Schema 的视图,可视作二维表,是在RDD的基础上添加了Schema的信息(包括数据集中的列、名称以及类型)。 DataFrame API提供了高层的关系操作,相对于RDD API要更加友好,降低了开发的成本。
DataSet
DataSet则是spark1.6提供的新抽象,是DataFrame的扩展。DataSet是强类型,用样例类来对DataSet中定义数据的结构信息,样例类属性直接映射到DataSet的字段。 DataFrame 是 DataSet 的特列,两者可以互转。
spark session
SparkSession 是 Spark 最新的 SQL 查询起始点,实质上是 SQLContext 和 HiveContext的组合,SparkSession 内部封装了 SparkContext,计算实际上是由 sparkContext 完成的。
想要使用sparkSQL必须以spark session为起点。
创建spark session
java
// 1. 方式一:通过SparkConf创建sparksession
import org.apache.spark.SparkConf;
import org.apache.spark.sql.*;
SparkConf sparkAppConf = new SparkConf().setMaster("local[*]").setAppName("sparkRddDemo");
SparkSession sparkSession = SparkSession.builder().config(sparkAppConf).getOrCreate();
// 2. 方式二:SparkSession直接创建sparksession
import org.apache.spark.SparkConf;
import org.apache.spark.sql.*;
SparkSession sparkSession = SparkSession.builder()
.appName("sparkSQLApp")
.config("spark.master", "local[2]")
.getOrCreate();
sparkSession.stop(); // 停止spark session
DataFrame使用
DataFrame需要通过spark session进行创建,创建方式有三种:
- 从数据源(文件系统、关系型数据库等)
- RDD
- hive table
从文件系统读取数据创建DataFrame
spark session的read()方法会返回一个reader对象(Java api下是DataFrameReader对象),reader对象有一个通用的读取文件的方法load()
,除此之外封装了一些特定文件的读取方法:
- load:泛用性文件读取方法,默认情况下读取parquet文件
- csv:读取csv文件,默认为逗号分隔
- json:json文件,可以每行一个json字符串,也可以是json的列表
- jdbc:读取JDBC
- orc:读取orc文件
- textFile:读取文本文件
- ...
本地有一个json文件:
json
{"username": "tom", "age": 10}
{"username": "John", "age": 20}
{"username": "Yang", "age": 40}
{"username": "Jerry", "age": 20}
通过load以及json两种方法读取这个json文件
java
// 1. 通过json方法读取json文件
Dataset<Row> ds = sparkSession.read().json("D:\\project\\sparkDemo\\inputs\\user.json");
// 可以看见这里返回的类型是Dataset<Row>,虽然写着DataSet,但是泛型是row,所以是dataframe数据
// 2. 通过load读取json文件
Dataset<Row> df = sparkSession.read().format("json").load("D:\\project\\sparkDemo\\inputs\\user.json");
// format方法传入一个文件类型字符串
df.show();
+---+--------+
|age|username|
+---+--------+
| 10| tom|
| 20| John|
| 40| Yang|
| 20| Jerry|
+---+--------+
RDD创建DataFrame
Java想要实现这个转换还是比较麻烦的,Scala中的RDD可以直接调用toDF()
转换。 普通的javaRDD需要转换为javaRDD<Row>
,然后还需要创建StructType
并添加字段信息,最终调用sparksession.createDataFrame()方法传入上述两个参数构建dataframe对象。
java
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.api.java.function.Function;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
import org.apache.spark.sql.RowFactory;
import org.apache.spark.sql.SparkSession;
import org.apache.spark.sql.types.DataTypes;
import org.apache.spark.sql.types.StructField;
import org.apache.spark.sql.types.StructType;
import java.util.ArrayList;
import java.util.List;
import static java.util.Arrays.asList;
public class sparkSQLdemo {
public static void main(String[] args) {
SparkConf sparkAppConf = new SparkConf().setMaster("local[*]").setAppName("sparkRddDemo");
SparkSession sparkSession = SparkSession.builder().config(sparkAppConf)
.getOrCreate();
// 需要确保只有一个sparkContext对象,否则会抛出异常
// 需要通过sparkSession.sparkContext()返回的Scala的sparkContext对象创建JavaSparkContext
JavaSparkContext sparkContext = JavaSparkContext.fromSparkContext(sparkSession.sparkContext());
List<String> stringList = asList("HELLO", "SPARK", "HELLO", "FLINK");
JavaRDD<String> rdd = sparkContext.parallelize(stringList);
// 将普通的javaRDD转换为javaRDD<Row>,因为转换后的dataframe是Dataset<Row>类型
JavaRDD<Row> RowRDD = rdd.map(new Function<String, Row>() {
@Override
public Row call(String s) throws Exception {
return RowFactory.create(s);
}
});
// 定义schema
List<StructField> fields = new ArrayList<>();
fields.add(DataTypes.createStructField("word", DataTypes.StringType, true));
StructType schema = DataTypes.createStructType(fields);
Dataset<Row> dataFrame = sparkSession.createDataFrame(RowRDD, schema);
dataFrame.show();
sparkSession.stop();
}
}
hivetable创建DataFrame
没有hive环境,跳过。
通过JDBC创建DataFrame
java
package src.main.sqlDemo;
import org.apache.spark.sql.*;
/**
* @projectName: sparkDemo
* @package: src.main.sqlDemo
* @className: spark01SQLBasic
* @author: NelsonWu
* @description: sparkSQL read mysql table and write to mysql
* @date: 2024/3/6 23:22
* @version: 1.0
*/public class spark04SQLJDBC {
public static void main(String[] args) throws AnalysisException {
SparkSession sparkSession = SparkSession.builder()
.appName("sparkSQLApp")
.config("spark.master", "local[2]")
.getOrCreate();
// 从MySQL加载表创建df
Dataset<Row> mysqlDF = sparkSession.read().format("jdbc")
.option("url", "jdbc:mysql://172.20.143.219:3306/test")
.option("driver", "com.mysql.cj.jdbc.Driver").option("user", "root")
.option("password", "mysql").option("dbtable", "ws").load();
mysqlDF.show();
// 将df新建MySQL表的形式写入MySQL数据库
mysqlDF.write().format("jdbc").option("url", "jdbc:mysql://172.20.143.219:3306/test")
.option("driver", "com.mysql.cj.jdbc.Driver").option("user", "root")
.option("password", "mysql").option("dbtable", "ws1").save();
sparkSession.stop();
}
}
DataFrame转RDD
想从dataframe转RDD非常简单直接调用toJavaRDD
方法即可,转换后的RDD对象为JavaRDD<Row>
类型
java
JavaRDD<Row> javaRDD = dataFrame.toJavaRDD();
javaRDD.foreach(new VoidFunction<Row>() {
@Override
public void call(Row row) throws Exception {
System.out.println(row.get(0));
}
});
SQL语法
要想在sparkSQL中使用sql,需要有表,然后我们需要通过SqlContext
这个上下文对象去调用sql()
方法执行sql语句。 通过dataFrame创建临时表:
java
ds.createTempView("user"); // 创建临时表,生命周期为当前session
ds.createOrReplaceTempView("tbname"); // 创建临时表,生命周期同上,如果存在同名表则替换该表,即数据替换成当前dataframe的数据
ds.createGlobalTempView("tbname"); // 创建全局的临时表,可供所有session使用
ds.createOrReplaceGlobalTempView("tbname"); // 同上,但是如果存在同名表会替换成当前的dataframe数据
可根据具体需求选用上述api。
java
Dataset<Row> ds = sparkSession.read().json("D:\\project\\sparkDemo\\inputs\\user.json");
ds.createTempView("user");
SQLContext sqlContext = ds.sqlContext();
sqlContext.sql("select username from user").show();
DSL语法
DataFrame 提供一个特定领域语言(domain-specific language, DSL)去管理结构化的数据。可以在 Scala, Java, Python 和 R 中使用 DSL,使用 DSL 语法风格不必去创建临时视图了。 DSL语法可以直接通过DataFrame对象调用DSL语法相关API使用,例如select/filter/groupBy/count
等。
下面是Scala的相关用法:
scala
df.select("username").show()
// 查询用户名称以及用户年龄+1的结果
df.select('username, 'age + 1).show()
// 过滤出年龄大于30的
df.filter($"age">30).show
// 按照年龄分组统计每个分组的数据量
df.groupBy("age").count.show
java
df.select("username", "age").show(); // 可以正常查询
// 下面这种写法是不正确的,select内的参数必须为df的列名
df.select("age + 1").show();
Java的API目前无法像Scala一样实现DSL语法(就目前按照我的查询的资料来看),但是其中还有其他的方法:selectExpr
可以实现复杂的查询
java
df.selectExpr("age + 1").show();
这样就可以实现年龄加一的操作了。 +---------+ |(age + 1)| +---------+ | 11| | 21| | 41| | 21| +---------+
可以参考下这个网页,里面有相关的案例,虽然是Scala的 sparkbyexamples.com/spark/spark...
数据保存
dataframe提供了相关的API将数据保存到外部(文件系统、关系型数据库等)。 Java中dataframe可以通过write
方法保存文件,write
方法会返回一个DataFrameWriter
对象,通过这个对象保存数据。 writer对象可以调用save方法保存,跟load方法一样,可以保存csv、json、orc、parquet、textfile等类型的文件,默认情况下也是parquet类型。 同时writer也单独实现了常见的数据文件方法,上述的基本都实现了,可以直接调用。
在save方法之前,可以通过mode方法传入保存模式 ![[Pasted image 20240308004428.png]] 默认情况下是error类型,目录存在则直接抛出异常提示文件已存在。
java
df.write()
.mode("overwrite")
.csv("/home/saberbin/output");// windows下会报错Hadoop未设置
df.write().mode("overwrite").format("csv").save("/home/saberbin/output");
// 通过format方法传入需要保存的文件类型,如果不传入,默认save方法保存的是parquet文件
请注意,如果是Windows系统下使用IDEA执行上述的代码,会抛出Hadoop未设置的异常,spark3.3.2环境。这个目前没有找到解决办法,可切换至Linux系统下。 保存到文件系统的情况下,save方法需要传入一个路径,其他情况可不传参数。 如果是保存到关系型数据库,需要通过option方法设置相关的参数
java
df.write().format("jdbc").option("url", "jdbc:mysql://172.20.143.219:3306/test")
.option("driver", "com.mysql.cj.jdbc.Driver").option("user", "root")
.option("password", "mysql").option("dbtable", "ws1").save();
DataSet使用
dataset是强类型
创建DataSet(df2ds)
可以通过样例类的序列转成DataSet,实际用处不是很大。 这里通过样例类将df转换为ds
java
import org.apache.spark.sql.*;
Dataset<Row> df = sparkSession.read().json("D:\\project\\sparkDemo\\inputs\\user.json");
Dataset<User> userDataset = df.as(Encoders.bean(User.class));
public static class User implements Serializable {
private String username;
private Integer age;
public User(String username, Integer age) {
this.username = username;
this.age = age;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}}
RDD转DataSet
官方都是通过spark session的createDataset
方法去创建dataset的:
java
List<String> data = Arrays.asList("abc", "abc", "xyz");
Dataset<String> ds = context.createDataset(data, Encoders.STRING());
Encoder<Tuple2<Integer, String>> encoder2 = Encoders.tuple(Encoders.INT(), Encoders.STRING());
List<Tuple2<Integer, String>> data2 = Arrays.asList(new scala.Tuple2(1, "a");
Dataset<Tuple2<Integer, String>> ds2 = context.createDataset(data2, encoder2);
但是这些都是基础数据类型的dataset,而不是自定义的样例类。 而且上述不是通过rdd转的,而是将序列直接转换成dataset对象。不太符合我的需求。 Scala是可以直接调用方法转换。Java目前还没有找到对应的方法,有一个折中的方案就是rdd->dataframe->dataset。但是我不太想用这个。
sparkSession的createDataset方法有三种传参方式:
其中两种都是传入序列或者列表的形式,一种是通过rdd传入创建dataset
前面由rdd转df的时候是通过添加结构化类型实现的:
java
// 定义schema
List<StructField> fields = new ArrayList<>();
fields.add(DataTypes.createStructField("word", DataTypes.StringType, true));
StructType schema = DataTypes.createStructType(fields);
Dataset<Row> dataFrame = sparkSession.createDataFrame(RowRDD, schema);// rdd -> dataframe
StructField只支持基础的数据类型,不能添加样例类这种自定义的类。 所以通过添加schema的只能是将rdd转成df,而不能变成ds。 之前想法是df是rdd添加了schema,ds是在df的基础上包装了强类型,那么我想在样例类RDD的基础上包装了row类型(df的基础),再转成ds应该可以,实际上试验过不行。
rdd转成列表再转换dataset
java
JavaRDD<String> stringJavaRDD = sparkContext.textFile("D:\\project\\sparkDemo\\inputs\\user.txt");
JavaRDD<User> rdd = stringJavaRDD.map(new Function<String, User>() {
@Override
public User call(String s) throws Exception {
String[] value = s.split(",");
String username = value[0];
String age = value[1];
return new User(username, Integer.valueOf(age));
}
});
Iterator<User> userIterator = rdd.toLocalIterator();
ArrayList<User> userList = new ArrayList<>();
while (userIterator.hasNext()){
userList.add(userIterator.next());
}
Dataset<User> dataset = sparkSession.createDataset(userList, Encoders.bean(User.class));
dataset.show();
rdd转df转ds
略
dataframe转dataset
样例类: 这里的age需要为Long类型
java
public static class User implements Serializable {
private String username;
private Long age;
public User(String username, Long age) {
this.username = username;
this.age = age;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Long getAge() {
return age;
}
public void setAge(Long age) {
this.age = age;
}}
java
Dataset<Row> df = sparkSession.read().json("D:\\project\\sparkDemo\\inputs\\user.json");
Dataset<User> dataset = df.as(Encoders.bean(User.class));
dataset.show();
因为这里自动推断了df的age为bigint,如果user类的age为Integet类型,则会抛出异常。
dataset转dataframe
dataset可以直接调用toDF
方法转换dataframe,该方法可以传入schema,也可以不传
java
Dataset<Row> dataframe = dataset.toDF();
UDF
通过 spark.udf 功能添加自定义函数, 实现自定义功能
java
Dataset<Row> ds = sparkSession.read().json("D:\\project\\sparkDemo\\inputs\\user.json");
ds.createTempView("user");
SQLContext sqlContext = ds.sqlContext();
sparkSession.udf().register("pefixName", new UDF1<String, String>() {
@Override
public String call(String s) throws Exception {
return "userName:" + s;
}
}, DataTypes.StringType);
sqlContext.sql("select pefixName(username) as name from user").show();
/*
+--------------+
| name|
+--------------+
| userName:tom|
| userName:John|
| userName:Yang|
|userName:Jerry|
+--------------+
*/
UDAF
强类型的实现方法:
java
import org.apache.spark.sql.expressions.Aggregator;
sparkSession.udf().register("calAvg", functions.udaf(new CalAvg(), Encoders.INT()));
sqlContext.sql("select calAvg(age) from user").show();
/*
+-----------+
|calavg(age)|
+-----------+
| 22|
+-----------+
*/
public static class Buff implements Serializable {
public Integer total;
public Integer cnt;
public Buff(){
} public Buff(Integer total, Integer cnt){
this.total = total;
this.cnt = cnt;
} public Integer getTotal() {
return total;
}
public void setTotal(Integer total) {
this.total = total;
}
public Integer getCnt() {
return cnt;
}
public void setCnt(Integer cnt) {
this.cnt = cnt;
}
public void addTotal(Integer total){
this.total = this.total + total;
} public void addCnt(){
this.cnt = this.cnt + 1;
}}
public static class CalAvg extends Aggregator<Integer, Buff, Integer>{
@Override
public Buff zero() {
/*
初始化缓冲区
*/ return new Buff(0, 0);
}
@Override
public Buff reduce(Buff b, Integer a) {
/*
更新缓冲区
*/ b.addTotal(a);
b.addCnt();
return b;
}
@Override
public Buff merge(Buff b1, Buff b2) {
/*
合并缓冲区
*/ Integer total = b1.getTotal() + b2.getTotal();
Integer cnt = b1.getCnt() + b2.getCnt();
return new Buff(total, cnt);
}
@Override
public Integer finish(Buff reduction) {
/*
计算结果
*/ return reduction.total / reduction.cnt;
}
@Override
public Encoder<Buff> bufferEncoder() {
/*
缓冲区编码操作
*/ return Encoders.bean(Buff.class);
}
@Override
public Encoder<Integer> outputEncoder() {
/*
输出结果的缓冲区操作
*/ return Encoders.INT();
}; }
强类型的UDAF也可以通过DSL语法进行调用,自定义的UDAF方法需要传入样例类对象,上述的UDAF修改一下即可,但是在我的Windows以及Linux(WSL)下都会抛出空指针异常(代码倒是没有问题),不知道是否为Java api下的bug。。。所以这里就不贴出代码了。