前面我们学习了如何在Spark Shell中使用SQL完成查询,现在我们通过IDEA编写Spark SQL查询程序。
Spark官网提供了两种方法来实现从RDD转换得到DataFrame,第一种方法是利用反射机制,推导包含某种类型的RDD,通过反射将其转换为指定类型的DataFrame,适用于提前知道RDD的schema。第二种方法通过编程接口与RDD进行交互获取schema,并动态创建DataFrame,在运行时决定列及其类型。
第一步:创建maven工程并导入依赖jar包
<properties>
<scala.version>2.11.8</scala.version>
<spark.version>2.2.0</spark.version>
</properties>
<dependencies>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>${scala.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.11</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.11</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>2.7.5</version>
</dependency>
</dependencies>
<build>
<sourceDirectory>src/main/scala</sourceDirectory>
<testSourceDirectory>src/test/scala</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<!-- <verbal>true</verbal>-->
</configuration>
</plugin>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>testCompile</goal>
</goals>
<configuration>
<args>
<arg>-dependencyfile</arg>
<arg>${project.build.directory}/.scala_dependencies</arg>
</args>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF
META-INF/*.DSA
META-INF/*.RSA
第一种方式创建DF:通过反射配合样例类推断Schema
Scala支持使用case class类型导入RDD转换为DataFrame,通过case class创建schema,case class的参数名称会被利用反射机制作为列名。这种RDD可以高效的转换为DataFrame并注册为表。
代码开发如下:
读取资料当中的person.txt,通过RDD,转换成为DF
import org.apache.spark.SparkContext
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{Column, DataFrame, Dataset, SparkSession}
case class Person(id:Int,name:String,age:Int)
object SparkSqlStudy {
def main(args: Array[String]): Unit = {
//第一步:获取sparkSession对象
val sparkSession: SparkSession = SparkSession.builder().appName("sparkSqlStudy").master("local[2]").getOrCreate()
//第二步:获取sparkContext
val sparkContext: SparkContext = sparkSession.sparkContext
sparkContext.setLogLevel("WARN")
//读取文件,生成RDD
val rddFile: RDD[String] = sparkContext.textFile("file:///F:\\scala与spark课件资料教案\\spark课程\\3、spark第三天\\资料\\person.txt")
//将每一行的内容按照空格切分,生成一个单词数组
val wordArray: RDD[Array[String]] = rddFile.map(_.split(" "))
val personRDD: RDD[Person] = wordArray.map(x => Person(x(0).toInt,x(1),x(2).toInt))
//导入隐式转换的类,我们的RDD才可以转换成DataFrame
import sparkSession.implicits._
//将我们的RDD转换成DataFrame
val personDF: DataFrame = personRDD.toDF()
/*********************************sparkSql DSL风格语法 开始**************************************/
personDF.printSchema()
personDF.show()
personDF.show(2)
println(personDF.head())
//查询name字段的所有值
personDF.select("name").show()
personDF.select($"name").show()
personDF.select(personDF.col("name")).show()
personDF.select(new Column("name")).show()
//将年龄的值进行 + 1
personDF.select($"id",$"name",$"age",$"age"+1).show()
//按照年龄进行分组
personDF.groupBy($"age").count().show()
/*********************************sparkSql DSL风格语法 结束**************************************/
/*********************************sparkSql sql风格语法 开始**************************************/
println("**************************")
val personTable: Unit = personDF.registerTempTable("t_person")
val personView: Unit = personDF.createTempView("t_person_view")
//查询表当中所有的数据
sparkSession.sql("select * from t_person").show()
sparkSession.sql("select * from t_person_view").show()
sparkSession.sql("select * from t_person_view where name = 'wangwu'").show()
/*********************************sparkSql sql风格语法 结束**************************************/
sparkContext.stop()
sparkSession.close()
}
}
第二种方式创建DF:通过StructType配合Row直接指定Schema
当case class不能提前定义好时,可以通过以下三步创建DataFrame
(1)将RDD转为包含Row对象的RDD
(2)基于StructType类型创建schema,与第一步创建的RDD相匹配
(3)通过sparkSession的createDataFrame方法对第一步的RDD应用schema创建DataFrame
import org.apache.spark.SparkContext
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.types.{IntegerType, LongType, StringType, StructType}
import org.apache.spark.sql.{DataFrame, Row, SparkSession}
object SparkSqlSchema {
def main(args: Array[String]): Unit = {
//创建sparkSession
val sparkSession: SparkSession = SparkSession.builder().appName("SparkSqlSchema").master("local[2]").getOrCreate()
//通过sparkSession构建SparkContext对象
val sparkContext: SparkContext = sparkSession.sparkContext
//设置日志级别
sparkContext.setLogLevel("WARN")
//读取文件内容,通过文件内容构建RDD
val fileRdd: RDD[String] = sparkContext.textFile("file:///F:\\scala与spark课件资料教案\\spark课程\\3、spark第三天\\资料\\person.txt")
//文本内容的每一行进行切分
val arrayRdd: RDD[Array[String]] = fileRdd.map(x => x.split(" "))
//将每一个数组里面的三个值取出来,构建row对象
val rowRdd: RDD[Row] = arrayRdd.map(x => Row(x(0).toInt,x(1),x(2).toInt))
//构建StructType对象
val struct:StructType = (new StructType).add("id", IntegerType, true, "id第一个字段").add("name", StringType, true, "name第二个字段").add("age", IntegerType, true, "age第三个字段")
//通过row以及structType来得到DataFrame
val personDataFrame: DataFrame = sparkSession.createDataFrame(rowRdd,struct)
//打印schema信息
personDataFrame.printSchema()
//将我们获取的DataFrame
val personView: Unit = personDataFrame.createTempView("t_person_view")
sparkSession.sql("select * from t_person_view").show()
sparkContext.stop()
sparkSession.close()
}
}
HiveContext是对应spark-hive这个项目,与hive有部分耦合, 支持hql,是SqlContext的子类,在Spark2.0之后,HiveContext和SqlContext在SparkSession进行了统一,可以通过操作SparkSession来操作HiveContext和SqlContext。
需求:创建hive表,加载student.csv,进行数据分析
第一步:添加pom依赖
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-hive_2.11</artifactId>
<version>2.2.0</version>
</dependency>
第二步:代码实现
import org.apache.spark.sql.SparkSession
object SparkHiveSql {
def main(args: Array[String]): Unit = {
val sparkSession: SparkSession = SparkSession
.builder()
.master("local[2]")
.appName("SparkHiveSql")
.enableHiveSupport()//开启hive的支持
.getOrCreate()
sparkSession.sql("create table if not exists student2(id int,name String,age int) row format delimited fields terminated by ','")
sparkSession.sql("load data local inpath './datas/student.csv' overwrite into table student2 ")
sparkSession.sql("select * from student2").show()
sparkSession.stop()
}
}
Spark SQL可以通过JDBC从关系型数据库中读取数据的方式创建DataFrame,通过对DataFrame一系列的计算后,还可以将数据再写回关系型数据库中。
第一步:添加jdbc连接驱动jar包
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
第二步:开发代码,读取mysql数据库当中的数据
object SparkMysql {
def main(args: Array[String]): Unit = {
//创建sparkSession对象
val sparkSession: SparkSession = SparkSession.builder().appName("sparkMySql").master("local[2]").getOrCreate()
val url ="jdbc:mysql://localhost:3306/userdb"
val tableName = "emp";
val properties = new Properties()
//注意:用户名和密码的属性只能是 user password
properties.setProperty("user","root")
properties.setProperty("password","admin")
val tableDatas: Unit = sparkSession.read.jdbc(url,tableName,properties).show()
sparkSession.close()
}
}
通过spark-shell运行
(1)、启动spark-shell(必须指定mysql的连接驱动包)
bin/spark-shell \
--master spark://node01:7077 \
--executor-memory 1g \
--total-executor-cores 2 \
--jars /export/servers/hive-1.1.0-cdh5.14.0/lib/mysql-connector-java-5.1.38.jar \
--driver-class-path /export/servers/hive-1.1.0-cdh5.14.0/lib/mysql-connector-java-5.1.38.jar
(2)、从mysql中加载数据
val mysqlDF = spark.read.format("jdbc").options(Map("url" -> "jdbc:mysql://192.168.0.107:3306/userdb", "driver" -> "com.mysql.jdbc.Driver", "dbtable" -> "emp", "user" -> "root", "password" -> "admin")).load()
(3)、执行查询
scala> mysqlDF.show
通过IDEA编写SparkSql代码
将person.txt文本文件写入到mysql数据库表当中去
思路解析:通过sparkContext读取文件,然后转换成RDD,将RDD转换成为DataFrame,然后注册成为一张表,查询出来数据,插入保存到mysql当中去
代码实现:
import java.util.Properties
import org.apache.spark.SparkContext
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{DataFrame, SaveMode, SparkSession}
case class Peson(id:Int,name:String,age:Int)
object Spark2Mysql {
/**
* 读取文本文件数据,然后写入到mysql数据库表当中去
* @param args
*/
def main(args: Array[String]): Unit = {
//获取sparkSession
val sparkSession: SparkSession = SparkSession.builder().appName("spark2Mysql").master("local[2]").getOrCreate()
//通过sparkSession得到sparkContext
val sparkContext: SparkContext = sparkSession.sparkContext
//通过sparkContext 读取文本文件内容,得到RDD
val arrRDD: RDD[Array[String]] = sparkContext.textFile("file:///F:\\scala与spark课件资料教案\\spark课程\\3、spark第三天\\资料\\person.txt").map(x => x.split(" "))
//通过RDD,配合样例类,将我们的数据转换成样例类对象
val personRDD: RDD[Person] = arrRDD.map(x => Person(x(0).toInt,x(1),x(2).toInt))
//导入sparkSession当中的隐式转换,将我们的样例类对象转换成DataFrame
import sparkSession.implicits._
val personDF: DataFrame = personRDD.toDF()
//打印dataFrame当中的数据
val personDFShow: Unit = personDF.show()
//将DataFrame注册成为一张表模型
val personView: Unit = personDF.createTempView("person_view")
//获取表当中的数据
val result: DataFrame = sparkSession.sql("select * from person_view")
//获取mysql连接
val url ="jdbc:mysql://localhost:3306/userdb"
val tableName = "person"
val properties = new Properties()
properties.setProperty("user","root")
properties.setProperty("password","admin")
//将我们查询的结果写入到mysql当中去
val jdbc: Unit = result.write.mode(SaveMode.Append).jdbc(url,tableName,properties)
sparkContext.stop()
sparkSession.close()
}
}
(2)用maven将程序打包
修改代码进行打包
修改master提交地址
//获取sparkSession
val sparkSession: SparkSession = SparkSession.builder()
.appName("spark2Mysql")
// .master("local[2]") 打包集群运行,不用再指定master的地址了,等会儿提交jar包的时候,我们手动指定
.getOrCreate()
修改文件读取路径
val arrRDD: RDD[Array[String]] = sparkContext.textFile(args(0)).map(x => x.split(" "))
(3)将Jar包提交到spark集群
spark-submit --master spark://node01:7077 \
--class cn.itcast.spark.sql.Spark2Mysql \
--executor-memory 1g \
--total-executor-cores 2 \
--jars /export/servers/hive-1.1.0-cdh5.14.0/lib/mysql-connector-java-5.1.38.jar \
--driver-class-path /export/servers/hive-1.1.0-cdh5.14.0/lib/mysql-connector-java-5.1.38.jar \
original-day03-1.0-SNAPSHOT.jar hdfs://node01.hadoop.com:8020/person.txt
Caused by: java.sql.SQLException: Cannot execute statement: impossible to write to binary log since BINLOG_FORMAT = STATEMENT and at least one table uses a storage engine limited to row-based logging. InnoDB is limited to row-logging when transaction isolation level is READ COMMITTED or READ UNCOMMITTED.
在sqlyog当中直接执行这一句即可
SET GLOBAL binlog_format=mixed;
Spark SQL主要目的是使得用户可以在Spark上使用SQL,其数据源既可以是RDD,也可以是外部的数据源(比如文本、Hive、Json等)。Spark SQL的其中一个分支就是Spark on Hive,也就是使用Hive中HQL的解析、逻辑执行计划翻译、执行计划优化等逻辑,可以近似认为仅将物理执行计划从MR作业替换成了Spark作业。SparkSql整合hive就是获取hive表中的元数据信息,然后通过SparkSql来操作数据
第一步:将hive-site.xml拷贝到spark安装家路径的conf目录下
node03执行以下命令来拷贝hive-site.xml到所有的spark安装服务器上面去
cd /export/servers/hive-1.1.0-cdh5.14.0/conf
cp hive-site.xml /export/servers/spark-2.2.0-bin-2.6.0-cdh5.14.0/conf/
scp hive-site.xml node02:/export/servers/spark-2.2.0-bin-2.6.0-cdh5.14.0/conf/
scp hive-site.xml node01:/export/servers/spark-2.2.0-bin-2.6.0-cdh5.14.0/conf/
第二步:将mysql的连接驱动包拷贝到spark的jars目录下
node03执行以下命令将连接驱动包拷贝到spark的jars目录下,三台机器都要进行拷贝
cd /export/servers/hive-1.1.0-cdh5.14.0/lib
cp mysql-connector-java-5.1.38.jar /export/servers/spark-2.2.0-bin-2.6.0-cdh5.14.0/jars/
scp mysql-connector-java-5.1.38.jar node02:/export/servers/spark-2.2.0-bin-2.6.0-cdh5.14.0/jars/
scp mysql-connector-java-5.1.38.jar node01:/export/servers/spark-2.2.0-bin-2.6.0-cdh5.14.0/jars/
第三步:测试sparksql整合hive是否成功
先启动hadoop集群,在启动spark集群,确保启动成功之后node01执行命令:
cd /export/servers/spark-2.2.0-bin-2.6.0-cdh5.14.0
bin/spark-sql --master spark://node01:7077 --executor-memory 1G --total-executor-cores 2
指明master地址、每一个executor的内存大小、一共所需要的核数、
mysql数据库连接驱动。
执行成功后的界面:进入到spark-sql 客户端命令行界面
查看当前有哪些数据库, 并创建数据库
show databases;
create database sparkhive;
在spark2.0版本后由于出现了sparkSession,在初始化sqlContext的时候,会设置默认的spark.sql.warehouse.dir=spark-warehouse,
此时将hive与sparksql整合完成之后,在通过spark-sql脚本启动的时候,还是会在哪里启动spark-sql脚本,就会在当前目录下创建一个spark.sql.warehouse.dir为spark-warehouse的目录,存放由spark-sql创建数据库和创建表的数据信息,与之前hive的数据息不是放在同一个路径下(可以互相访问)。但是此时spark-sql中表的数据在本地,不利于操作,也不安全。
所有在启动的时候需要加上这样一个参数:
–conf spark.sql.warehouse.dir=hdfs://node01:8020/user/hive/warehouse
保证spark-sql启动时不在产生新的存放数据的目录,sparksql与hive最终使用的是hive同一存放数据的目录。
如果使用的是spark2.0之前的版本,由于没有sparkSession,不会有spark.sql.warehouse.dir配置项,不会出现上述问题。
node01执行以下命令重新进去spark-sql
cd /export/servers/spark-2.2.0-bin-2.6.0-cdh5.14.0
bin/spark-sql \
--master spark://node01:7077 \
--executor-memory 1G --total-executor-cores 2 \
--conf spark.sql.warehouse.dir=hdfs://node01:8020/user/hive/warehouse
进入到sparkSQL之后便可以进行sparkSQL的开发了,但是实际工作当中我们一般都是将写好的HQL语句放到脚本文件里面去,然后通过定时执行脚本文件即可
第一步:准备表数据
node01服务器准备表数据
cd /export/servers
hdfs dfs -mkdir -p /sparkhivedatas
vim sparkhive.txt
1 zhangsan 18
2 lisi 28
3 wangwu 20
4 zhaoliu 38
5 chenqi 45
将数据上传到hdfs上面去
cd /export/servers
hdfs dfs -put sparkhive.txt /sparkhivedatas
第二步:开发sparkSQL脚本,并加载表数据
node01执行以下命令开发sparkSQL脚本
cd /export/servers
vim sparkhive.sql
create database if not exists sparkhivedb;
create table if not exists sparkhivedb.user(id int,name string,age int) row format delimited fields terminated by ' ';
load data inpath 'hdfs://node01:8020/sparkhivedatas' overwrite into table sparkhivedb.user;
drop table if exists sparkhivedb.userresult;
create table sparkhivedb.userresult as select * from sparkhivedb.user where age > 30;
第三步:开发shell脚本,使用shell脚本执行sparkSQL脚本
node01执行以下命令开发sparkSQL脚本
cd /export/servers
vim sparksql.sh
#!/bin/bash
SPARK_SQL="/export/servers/spark-2.2.0-bin-2.6.0-cdh5.14.0/bin/spark-sql --master spark://node01:7077 --executor-memory 1G -
-total-executor-cores 2 --conf spark.sql.warehouse.dir=hdfs://node01:8020/user/hive/warehouse"
$SPARK_SQL -f /export/servers/sparkhive.sql
#或者我们也可以直接将sql语句写到shell脚本里面通过 -e 来进行执行,例如
SPARK_HQL="create database if not exists sparkhqldb; create table if not exists sparkhqldb.myuser(id int,name string,age int)
row format delimited fields terminated by ' ' ;"
$SPARK_SQL -e "$SPARK_HQL"