14.sparkSql知识点

一、sparkcore的复习

一. spark的简介
	1. spark是scala语言编写的一个计算框架
	2. spark是一个快速的,通用的,运行在分布式上的一个大数据集的计算分析框架
	3. 快速的原因就是因为spark处理的数据是基于内存存储的(与MR相比的非常重要的区别)
	4. spark的组件包括:
		sparkcore(提供了RDD的通用编程模型),
		sparksql(交互式编程),
		sparkStreaming(流式处理),
		GraphX(图计算),
		MLlib(机器学习)
	5. 与MR的区别:
    	- 存储位置: spark是内存, MR是基于磁盘
    	- 容错性(失败时):	
    		spark通过血缘关系找到上一个RDD重新计算即可,无需从头开始
    		MR的job某一部分失败后,会从头开始
    	- 通用性:spark提供了两大类的RDD的API,以及流处理,图计算等变成模型
    			MR只提供了Map阶段和Reduce阶段的简单编程模型
    	- 框架复杂度:由于spark的编程模型以及内部组件众多,所以复杂
    				MR只提供了Map阶段和Reduce阶段的简单编程模型
    				
    	MR也不是没有优点:和spark比较起来,生态简单,运行稳定,适合后台长期运行			
二. spark的部署(作业运行位置,调度工具)
	-. 部署方式:
		local:  只有master(或者说是本地操作系统)
		standalone:主从架构(一个master,多个worker)
		yarn: hadoop的一个资源管理器
		mesos:apache的一个调度工具
		k8s: 企业中常用的一款性能非常好的调度工具
	- standalone模式的搭建
三. spark作业提交:
	1. 使用spark-submit指令提交作业,以及各种参数等
		--master
		--class
		--executor-cores
		--executor-memory
		.....
		注意:如果不指定executor-cores,默认总数是2个
     2. standalone的提交方法:也有两种client,cluster
     	spark-submit  
     	--class classFullName  
     	--master url
     	--deploy-mode client|cluster
     	jarPath 100
     3. yarn方法的提交:也有两种client,cluster
     	spark-submit  
     	--class classFullName  
     	--master yarn
     	--deploy-mode client|cluster
     	jarPath 100
四. spark-shell
	1. 是spark提供了一个shell交互界面,可以进行简单的编程
	2. 还提供了webui访问端口
	3. 还提供了SparkContext的变量
	4. 也有local和cluster模式
		cluster模式使用的是standalone集群 
五. RDD的简介
	1. 弹性分布式数据集(resilient distributed dataset),是数据的抽象概念,也可以理解为是数据的引用
	2. 三大特征:不可变的(只读),可分区的,并行计算
	3. 结构:
		abstract class	RDD{
            partitions:分区列表
            sparkContext: 上下文
            sparkConf: 配置对象
            compute:   计算每一个分区的函数
            dependencies:依赖关系
            partitioner:分区器
            preferred location: 要分析的数据的优先位置
            checkpiont:是否进行了检查点设置
            store level:存储级别
            iterator: 用于从内存中读取真正的数据
        }    
	4.RDD的弹性特点:
		-存储的弹性
		-容错的弹性
		-重试次数的弹性
		-重新分片
	5. RDD的分类:
    	PairRDD 指的都是KV形式的RDD
六. RDD的编程
	1. 其实就是算子的使用
	2. 算子分类:两大类
		- 转换transformation算子
			功能:将一个RDD转成一个新的RDD
			特点:分窄依赖算子和宽依赖算子
				 窄依赖:上游RDD的一个分区对应下游RDD的一个分区
				 宽依赖:上游RDD的一个分区对应下游RDD的多个分区,里面都是有shuffle机制
				 
				 stage的划分一定是宽依赖造成(划分)的
				 但是宽依赖算子不一定会划分stage
				 rdd1.sortByKey().groupByKey().groupByKey()
		- 行动Action算子
			触发一个job
			将RDD的数据发送给Driver或者是搜集后输出
七. spark深入了解
	1. 依赖关系:
		宽和窄依赖
	2. DAG:  有开始节点和结束节点,但是所有的节点没有闭环情况。就是一个有向无环图
		     DAG的结束节点一定是一个action算子,正好是一个job的划分
			 血统关系:在出错时,方便追溯上游的RDD
	3. job、stage、taskSet的划分
		 stage的划分:是一个宽依赖算子
		 taskSet:每一个stage中都会多个Task. task的个数由分区数决定
	4. cache、persist、checkpoint
		cache(): 缓存到内存		缓存的数据都会自动删除   		 官方建议使用
		persist(....):可以指定缓存等级   缓存的数据都会自动删除   官方建议使用
		checkPoint: 支持化到磁盘上,需要指定路径。持久化的数据需要手动删除
	5. 广播变量:
		减少Driver向Task发送的数据的网络IO,向executor发送一份	
	6. 累加器:
		driver里的变量想要获取每一个executor里局部信息的汇总,需要搜集。
		而累加器就可以实现局部的累加以及向driver端汇报
	7. 自定义排序:
		- 简单排序
		- 要排序的类型实现Ordered特质
		- 使用隐式参数进行排序:  Ordering类型的对象
	8. 分区器:
		HashPartitioner
		RangePartitioner
		自定义:需要实现Partitioner特质
	9. 文件的输入输出
		- 纯文本文件: textFile	saveAsTextFile
		- json :textFile		saveAsTextFile
		- csv	:textFile		saveAsTextFile
		- sequence: sc.sequence     saveAsSequenceFile
	10. JDBCRDD:可以连接数据库,进行范围查询操作

二、sparksql的简介

2.1 简介

1. sparksql是spark中的一个重要组件
2. 构建在SparkCore基础之上的一个基于SQL的计算模块。
3. 前身叫shark,基于hive,hive的发展制约了shark的发展
4. 后来shark项目停止了,独立出来一个新的,叫sparksql,带有shark的优点:基于内存的列式存储,动态字节码优化技术
5. 用于处理具有结构化的大数据集

2.2 特点

1. 集成:
	可以在spark项目上使用sql,可以使用java、scala、python、r语言
2. 统一的数据源访问:
	使用通用的方法访问各种数据源:avro,parquet,orc,json,jdbc等
3. 集成了sql和hivesql
	支持普通的sql以及hivesql在hive warehouse上运行。
4. 标准的连接方式
	可以使用jdbc或者是odbc连接sparksql

三、sparksql的编程模型和入口

3.1 两种编程模型(也可以说成风格)

3.1.1 SQL编程模型(SQL风格)

就是使用我们学过的通用的sql(包括标准sql以及hivesql)语句进行执行数据的查询和计算

注意:如果想要使用这种写法,那么sparksql中必须要将对应的数据维护成一个表。

3.1.2 DataFrame和DataSet编程模型(DSL风格)

1. DataFrame 是spark1.3以后出现的, DataSet是spark1.6以后出现的
2. DataFrame和DataSet都是对真正要处理的数据的一个抽象,和RDD概念相似。
3. DataFrame和DataSet比RDD多了一个schema描述。
4. schema指的就是表名,表头,字段,字段类型等描述信息

称呼:
RDD编程称之为spark第一代编程模型
DataFrame比RDD多了一个Schema元数据信息,被称之为Spark体系中的第二代编程模型
Dataset吸收了RDD的优点(强类型推断和强大的函数式编程)和DataFrame中的优化(SQL优化引擎,内存列存储),成为Spark的最新一代的编程模型。

3.2 RDD & DataFrame & DataSet的比较

1. RDD和DataFrame的比较
   - RDD描述的是数据结构,以及提供了操作方式,并且具有弹性特点,也可以理解为RDD是一张二维表
   - DataFrame以前叫SchemaRDD,从名字上就可以知道,比RDD多了一个Schema,而schema就是元数据
   	 元数据有表名,表头,字段,字段类型
   - DataFrame易用性更好,底层可以自动优化。即使你写的sql比较复杂,运行速度也非常快
2. RDD和DataSet的比较
	- 相同点:DataSet也引入了RDD的强类型推断,也是在RDD的每行数据加了类型约束
	- 不同点:DataSet还可以映射成java对象
	运行时,DataSet也会自动优化
3. DataFrame与DataSet的比较
	相同点:都有schema
	不同点:DataFrame没有编译器检查机制
		  DataSet有编译器检查机制

3.3 编程入口

1. 在sparkSql中,不再使用SparkConext,但是也会依赖于SparkContext。
2. 在Spark2.0以前,具有两个入口,分别是
	- SqlContext,就是提供了一些算子及其普通操作
	- HiveContext:是SqlContext的子类,提供了一些特殊的方法,比如开窗函数等
3. 在Spark2.0以后,将两者进行合并,成了一个SparkSession,SparkSession的构建会使用工厂方法。

pom依赖

<properties>
    <scala.version>2.11.8</scala.version>
    <spark.version>2.2.3</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-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>
</dependencies>
package com.qf.sql.day01

import org.apache.spark.sql.hive.HiveContext
import org.apache.spark.sql.{DataFrame, SQLContext, SparkSession}
import org.apache.spark.{SparkConf, SparkContext}

/**
 * sparksql的程序入口方法
 */
object AppAccessDemo {
    def main(args: Array[String]): Unit = {
        //spark2.0以前,有两个,分别是sqlContext和hiveContext
        val conf: SparkConf = new SparkConf().setMaster("local").setAppName("test")
        val sc = new SparkContext(conf)

        //第一种:获取一个sqlContext
//        val sqlContext = new SQLContext(sc)
//        val df: DataFrame = sqlContext.read.json("data/a.json")
//        df.show()

        //第二种:获取一个hiveContext
//        val hiveContext = new HiveContext(sc)
//        val frame: DataFrame = hiveContext.table("")
//        frame.show()

        //第三种:使用spark2.0以后的SparkSession对象
        val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
        val df: DataFrame = spark.read.json("data/a.json")
        df.show()
        sc.stop()

    }
}

四、sparksql的基本编程

4.1 SparkSession的三种创建方式

package com.qf.sql.day01

import org.apache.spark.SparkConf
import org.apache.spark.sql.{DataFrame, SparkSession}

object _02SparkSessionCreateMethod {
    def main(args: Array[String]): Unit = {
        //第一种方法: 直接使用SparkSession的builder构建器,如果想要设置配置信息,可以调用.config(String,String),多参数的时候可以连续调用
       /* val spark: SparkSession = SparkSession.builder()
          .appName("test")
//          .config("master","local")
//          .config("","")
//          .config("","")
          .master("local")
          .getOrCreate()
        val df: DataFrame = spark.read.json("data/a.json")
        df.show()*/


        //第二种方法:就是使用SparkConf绑定配置信息
       /* val conf = new SparkConf().setMaster("local").setAppName("test").set("a","true")
        val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
        spark.read.json("data/a.json").show()*/


        //第三种方法:获取连接hive的SparkSession对象
        val spark: SparkSession = SparkSession.builder().master("local")
          .appName("test")
          .enableHiveSupport()      //开启访问hive的支持
          .getOrCreate()
        spark.table("").show()

        spark.stop()
    }
}

4.2 SparkSql的常用方法

4.2.1 几个常用的算子

package com.qf.sql.day01

import org.apache.spark.sql.types.StructType
import org.apache.spark.sql.{Column, DataFrame, SparkSession}

object _03SparkSqlFirst {
    def main(args: Array[String]): Unit = {
        //获取SparkSession对象
        val spark = SparkSession.builder().appName("test").master("local").getOrCreate()
        import spark.implicits._
        //读取一个json文件,返回DataFrame对象,可以当成一个二维表
        val df: DataFrame = spark.read.json("data/emp.json")

        /**
         * show(): 是一个行动算子,用于将结果打印到控制台上
         * 无参数方法:表示列出Top20条
         *
         * show(numrows:Int)  :可以指定要显示的行数
         * show(numRows:Int,truncate:boolean) :第二个参数表示字段的值如果超出了20个字符,是否要截断显示
         *    true:表示截断,右对齐
         *    false:表示全显示,左对齐
         *
         *  相当于:select * from ....
         */
//        df.show(10,false)


        //显示schema: schema是元数据,即表名,表头,字段名,字段类型
//        val schema: StructType = df.schema
//        schema.printTreeString()
        //简单写法,打印schema
//        df.printSchema()

        //select算子:  可以指定字段名称
//        val df1: DataFrame = df.select("empno", "ename", "sal", "deptno")
//        val df1: DataFrame = df.select(new Column("empno"),new Column("ename"),new Column("job"))
        //如果想要使用$,需要导入SparkSession的隐式转换,  建议,不管是否使用隐式转换,也应该导入
//        import spark.implicits._
//        val df1: DataFrame = df.select($"empno",$"ename",$"job")
//        df1.show()


        //where算子: 用于指定条件的
       /* val df2: DataFrame = df.select("empno", "ename", "sal", "deptno")
                                .where("deptno<30")
        df2.show()
        val df3: DataFrame = df.select("empno", "ename", "sal", "deptno")
          .where("deptno<30 and sal<3000")
        df3.show()
        val df4: DataFrame = df.select("empno", "ename", "sal", "deptno")
          .where("deptno<30")
          .where("sal<3000")
        df4.show()*/

        //字段可以进行计算
       /* val df5: DataFrame = df.select($"empno", $"ename",$"sal", $"sal"+1000, $"deptno")
        val df5: DataFrame = df.select($"empno", $"ename",$"sal", $"sal".+(1000), $"deptno")
        df5.show()*/

        //取别名
//        df.select($"empno", $"ename",$"sal".as("salary"), ($"sal"+1000).as("涨薪后"), $"deptno").show()

        //分组算子:groupBy,
        // 求总数: count
        df.groupBy("deptno").count().show()
        spark.stop()
    }
}

4.2.2 其他Action操作

show

以表格的形式在输出中展示DataFrame中的数据,该方法有四种调用方式,分别为:

(1)show
  只显示前20条记录。且过长的字符串会被截取

scala> employeeDF.show
+---+-----+------+--------+---------+
|age|depId|gender|    name|   salary|
+---+-----+------+--------+---------+
| 25|    1|  male|     Leo|20000.126|
| 30|    2|female|   Marry|  25000.0|
| 35|    1|  male|    Jack|  15000.0|
| 42|    3|  male|     Tom|  18000.0|
| 21|    3|female|  Kattie|  21000.0|
| 19|    2|  male|     Jen|   8000.0|
| 30|    2|female|     Jen|  28000.0|
| 42|    3|  male|     Tom|  18000.0|
| 18|    3|female|XiaoFang|  58000.0|
+---+-----+------+--------+---------+

(2)show(numRows: Int)

显示numRows条

scala> employeeDF.show(3)
+---+-----+------+-----+---------+
|age|depId|gender| name|   salary|
+---+-----+------+-----+---------+
| 25|    1|  male|  Leo|20000.126|
| 30|    2|female|Marry|  25000.0|
| 35|    1|  male| Jack|  15000.0|
+---+-----+------+-----+---------+
only showing top 3 rows

(3)show(truncate: Boolean)
  是否截取20个字符,默认为true。

scala> employeeDF.show(true)
+---+-----+------+--------+---------+
|age|depId|gender|    name|   salary|
+---+-----+------+--------+---------+
| 25|    1|  male|     Leo|20000.126|
| 30|    2|female|   Marry|  25000.0|
| 35|    1|  male|    Jack|  15000.0|
| 42|    3|  male|     Tom|  18000.0|
| 21|    3|female|  Kattie|  21000.0|
| 19|    2|  male|     Jen|   8000.0|
| 30|    2|female|     Jen|  28000.0|
| 42|    3|  male|     Tom|  18000.0|
| 18|    3|female|XiaoFang|  58000.0|
+---+-----+------+--------+---------+

(4)show(numRows: Int, truncate: Int)
  显示记录条数,以及截取字符个数,为0时表示不截取

scala> employeeDF.show(3, 2)
+---+-----+------+----+------+
|age|depId|gender|name|salary|
+---+-----+------+----+------+
| 25|    1|    ma|  Le|    20|
| 30|    2|    fe|  Ma|    25|
| 35|    1|    ma|  Ja|    15|
+---+-----+------+----+------+
only showing top 3 rows
collect

获取DataFrame中的所有数据并返回一个Array对象

scala> employeeDF.collect
res6: Array[org.apache.spark.sql.Row] = Array([25,1,male,Leo,20000.126], [30,2,female,Marry,25000.0], [35,1,male,Jack,15000.0], [42,3,male,Tom,18000.0], [21,3,female,Kattie,21000.0], [19,2,male,Jen,8000.0], [30,2,female,Jen,28000.0], [42,3,male,Tom,18000.0], [18,3,female,XiaoFang,58000.0])
collectAsList

功能和collect类似,只不过将返回结构变成了List对象

scala> employeeDF.collectAsList
res7: java.util.List[org.apache.spark.sql.Row] = [[25,1,male,Leo,20000.126], [30,2,female,Marry,25000.0], [35,1,male,Jack,15000.0], [42,3,male,Tom,18000.0], [21,3,female,Kattie,21000.0], [19,2,male,Jen,8000.0], [30,2,female,Jen,28000.0], [42,3,male,Tom,18000.0], [18,3,female,XiaoFang,58000.0]]
describe

这个方法可以动态的传入一个或多个String类型的字段名,结果仍然为DataFrame对象,用于统计数值类型字段的统计值,比如count, mean, stddev, min, max等。

scala> employeeDF.describe("age", "salary").show()
+-------+-----------------+------------------+
|summary|              age|            salary|
+-------+-----------------+------------------+
|  count|                9|                 9|
|   mean|29.11111111111111|23444.458444444444|
| stddev|9.198429817697752|14160.779261027332|
|    min|               18|            8000.0|
|    max|               42|           58000.0|
+-------+-----------------+------------------+
first, head, take, takeAsList

(1)first获取第一行记录
(2)head获取第一行记录,head(n: Int)获取前n行记录
(3)take(n: Int)获取前n行数据
(4)takeAsList(n: Int)获取前n行数据,并以List的形式展现
以Row或者Array[Row]的形式返回一行或多行数据。first和head功能相同。
take和takeAsList方法会将获得到的数据返回到Driver端,所以,使用这两个方法时需要注意数据量,以免Driver发生OutOfMemoryError。

以上方法简单,使用过程和结果略。

4.2.3 条件查询和Join操作

以下返回为DataFrame类型的方法,可以连续调用。

where条件

等同于SQL语言中where关键字。传入筛选条件表达式,可以用and和or。得到DataFrame类型的返回结果。

scala> employeeDF.where("depId = 3").show
+---+-----+------+--------+-------+
|age|depId|gender|    name| salary|
+---+-----+------+--------+-------+
| 42|    3|  male|     Tom|18000.0|
| 21|    3|female|  Kattie|21000.0|
| 42|    3|  male|     Tom|18000.0|
| 18|    3|female|XiaoFang|58000.0|
+---+-----+------+--------+-------+

scala> employeeDF.where("depId = 3 and salary > 18000").show
+---+-----+------+--------+-------+
|age|depId|gender|    name| salary|
+---+-----+------+--------+-------+
| 21|    3|female|  Kattie|21000.0|
| 18|    3|female|XiaoFang|58000.0|
+---+-----+------+--------+-------+
filter过滤

传入筛选条件表达式,得到DataFrame类型的返回结果。和where使用条件相同。

scala> employeeDF.filter("depId = 3 or salary > 18000").show
+---+-----+------+--------+---------+
|age|depId|gender|    name|   salary|
+---+-----+------+--------+---------+
| 25|    1|  male|     Leo|20000.126|
| 30|    2|female|   Marry|  25000.0|
| 42|    3|  male|     Tom|  18000.0|
| 21|    3|female|  Kattie|  21000.0|
| 30|    2|female|     Jen|  28000.0|
| 42|    3|  male|     Tom|  18000.0|
| 18|    3|female|XiaoFang|  58000.0|
+---+-----+------+--------+---------+
查询指定字段

select

根据传入的String类型字段名,获取指定字段的值,以DataFrame类型返回。

scala> employeeDF.select("name", "age").show(3)
+-----+---+
| name|age|
+-----+---+
|  Leo| 25|
|Marry| 30|
| Jack| 35|
+-----+---+
only showing top 3 rows

还有一个重载的select方法,不是传入String类型参数,而是传入Column类型参数。可以实现select id, id+1 from table这种逻辑。

scala> employeeDF.select(employeeDF("age"), employeeDF("age") + 1 ).show(5)
+---+---------+
|age|(age + 1)|
+---+---------+
| 25|       26|
| 30|       31|
| 35|       36|
| 42|       43|
| 21|       22|
+---+---------+
only showing top 5 rows

能得到Column类型的方法是column和col方法,col方法更简便一些。

scala> employeeDF.select(col("age"), col("age") + 1 ).show(5)
+---+---------+
|age|(age + 1)|
+---+---------+
| 25|       26|
| 30|       31|
| 35|       36|
| 42|       43|
| 21|       22|
+---+---------+
only showing top 5 rows

selectExpr

可以直接对指定字段调用UDF函数,或者指定别名等。传入String类型参数,得到DataFrame对象。

scala> employeeDF.selectExpr("name" , "depId as departmentId" , "round(salary)").show()
+--------+------------+----------------+
|    name|departmentId|round(salary, 0)|
+--------+------------+----------------+
|     Leo|           1|         20000.0|
|   Marry|           2|         25000.0|
|    Jack|           1|         15000.0|
|     Tom|           3|         18000.0|
|  Kattie|           3|         21000.0|
|     Jen|           2|          8000.0|
|     Jen|           2|         28000.0|
|     Tom|           3|         18000.0|
|XiaoFang|           3|         58000.0|
+--------+------------+----------------+

col

只能获取一个字段,返回对象为Column类型。

scala> employeeDF.select(col("name")).show(3)
+-----+
| name|
+-----+
|  Leo|
|Marry|
| Jack|
+-----+
only showing top 3 rows

drop

返回一个新的DataFrame对象,其中不包含去除的字段,一次只能去除一个字段。

scala> employeeDF.drop("gender").show(3)
+---+-----+-----+---------+
|age|depId| name|   salary|
+---+-----+-----+---------+
| 25|    1|  Leo|20000.126|
| 30|    2|Marry|  25000.0|
| 35|    1| Jack|  15000.0|
+---+-----+-----+---------+
only showing top 3 rows
limit

limit方法获取指定DataFrame的前n行记录,得到一个新的DataFrame对象。和take与head不同的是,limit方法不是Action操作。

scala> employeeDF.limit(3).show
+---+-----+------+-----+---------+
|age|depId|gender| name|   salary|
+---+-----+------+-----+---------+
| 25|    1|  male|  Leo|20000.126|
| 30|    2|female|Marry|  25000.0|
| 35|    1|  male| Jack|  15000.0|
+---+-----+------+-----+---------+
order by

orderBy和sort

按指定字段排序,默认为升序。加个“-”号表示降序排序。只对数字类型和日期类型生效。sort和orderBy使用方法相同。

scala> employeeDF.orderBy(- col("salary")).show(3)
scala> employeeDF.sort(- col("salary")).show(3)
+---+-----+------+--------+-------+
|age|depId|gender|    name| salary|
+---+-----+------+--------+-------+
| 18|    3|female|XiaoFang|58000.0|
| 30|    2|female|     Jen|28000.0|
| 30|    2|female|   Marry|25000.0|
+---+-----+------+--------+-------+
only showing top 3 rows

sortWithinPartitions

和上面的sort方法功能类似,区别在于sortWithinPartitions方法返回的是按Partition排好序的DataFrame对象。

val repartitioned = employeeDF.repartition(2)
repartitioned.sortWithinPartitions("salary").show
+---+-----+------+--------+---------+
|age|depId|gender|    name|   salary|
+---+-----+------+--------+---------+
| 35|    1|  male|    Jack|  15000.0|
| 25|    1|  male|     Leo|20000.126|
| 21|    3|female|  Kattie|  21000.0|
| 30|    2|female|     Jen|  28000.0|
| 18|    3|female|XiaoFang|  58000.0|
| 19|    2|  male|     Jen|   8000.0|
| 42|    3|  male|     Tom|  18000.0|
| 42|    3|  male|     Tom|  18000.0|
| 30|    2|female|   Marry|  25000.0|
+---+-----+------+--------+---------+
group by

groupBy

根据指定字段进行group by操作,可以指定多个字段。

groupBy方法有两种调用方式,可以传入String类型的字段名,也可传入Column类型的对象。

employeeDF.groupBy("depId").count.show
employeeDF.groupBy(employeeDF("depId")).count.show
+-----+-----+
|depId|count|
+-----+-----+
|    1|    2|
|    3|    4|
|    2|    3|
+-----+-----+

cube和rollup

功能类似于SQL中的group by cube/rollup。

cube:为指定表达式集的每个可能组合创建分组集。首先会对(A、B、C)进行group by,然后依次是(A、B),(A、C),(A),(B、C),(B),( C),最后对全表进行group by操作。

rollup:在指定表达式的每个层次级别创建分组集。group by A,B,C with rollup首先会对(A、B、C)进行group by,然后对(A、B)进行group by,然后是(A)进行group by,最后对全表进行group by操作。

employeeDF.cube("depId","gender").sum("salary").show()
+-----+------+------------------+
|depId|gender|       sum(salary)|
+-----+------+------------------+
|    3|female|           79000.0|
|    1|  male|35000.126000000004|
|    1|  null|35000.126000000004|
| null|  null|        211000.126|
|    3|  null|          115000.0|
|    2|  male|            8000.0|
|    3|  male|           36000.0|
| null|female|          132000.0|
|    2|  null|           61000.0|
| null|  male|         79000.126|
|    2|female|           53000.0|
+-----+------+------------------+

employeeDF.rollup("depId","gender").sum("salary").show()
+-----+------+------------------+
|depId|gender|       sum(salary)|
+-----+------+------------------+
|    3|female|           79000.0|
|    1|  male|35000.126000000004|
|    1|  null|35000.126000000004|
| null|  null|        211000.126|
|    3|  null|          115000.0|
|    2|  male|            8000.0|
|    3|  male|           36000.0|
|    2|  null|           61000.0|
|    2|female|           53000.0|
+-----+------+------------------+

GroupedData对象

该方法得到的是GroupedData类型对象,在GroupedData的API中提供了group by之后的操作,比如:

  • max(colNames: String*),获取分组中指定字段或者所有的数字类型字段的最大值,只能作用于数字类型字段

  • min(colNames: String*),获取分组中指定字段或者所有的数字类型字段的最小值,只能作用于数字类型字段

  • mean(colNames: String*),获取分组中指定字段或者所有的数字类型字段的平均值,只能作用于数字类型字段

  • sum(colNames: String*),获取分组中指定字段或者所有的数字类型字段的和值,只能作用于数字类型字段

  • count(),获取分组中的元素个数

    运行结果示例:

    employeeDF.groupBy("depId").count.show
    +-----+-----+
    |depId|count|
    +-----+-----+
    |    1|    2|
    |    3|    4|
    |    2|    3|
    +-----+-----+
    
    employeeDF.groupBy("depId").max("salary").show
    +-----+-----------+
    |depId|max(salary)|
    +-----+-----------+
    |    1|  20000.126|
    |    3|    58000.0|
    |    2|    28000.0|
    +-----+-----------+
    
distinct

distinct

返回当前DataFrame中不重复的Row记录。

employeeDF.distinct
+---+-----+------+--------+---------+
|age|depId|gender|    name|   salary|
+---+-----+------+--------+---------+
| 35|    1|  male|    Jack|  15000.0|
| 30|    2|female|     Jen|  28000.0|
| 19|    2|  male|     Jen|   8000.0|
| 18|    3|female|XiaoFang|  58000.0|
| 25|    1|  male|     Leo|20000.126|
| 42|    3|  male|     Tom|  18000.0|
| 30|    2|female|   Marry|  25000.0|
| 21|    3|female|  Kattie|  21000.0|
+---+-----+------+--------+---------+

dropDuplicates

根据指定字段去重。类似于select distinct a, b操作。

employeeDF.dropDuplicates(Seq("name")).show
+---+-----+------+--------+---------+
|age|depId|gender|    name|   salary|
+---+-----+------+--------+---------+
| 35|    1|  male|    Jack|  15000.0|
| 42|    3|  male|     Tom|  18000.0|
| 19|    2|  male|     Jen|   8000.0|
| 30|    2|female|   Marry|  25000.0|
| 21|    3|female|  Kattie|  21000.0|
| 25|    1|  male|     Leo|20000.126|
| 18|    3|female|XiaoFang|  58000.0|
+---+-----+------+--------+---------+
聚合

聚合操作调用的是agg方法,该方法有多种调用方式。一般与groupBy方法配合使用。
以下示例其中最简单直观的一种用法,对age字段求最大值,对salary字段求和。

employeeDF.agg("age" -> "max", "salary" -> "sum").show
+--------+-----------+
|max(age)|sum(salary)|
+--------+-----------+
|      42| 211000.126|
+--------+-----------+
union

对两个DataFrame进行合并

employeeDF.union(employeeDF.limit(1)).show
+---+-----+------+--------+---------+
|age|depId|gender|    name|   salary|
+---+-----+------+--------+---------+
| 25|    1|  male|     Leo|20000.126|
| 30|    2|female|   Marry|  25000.0|
| 35|    1|  male|    Jack|  15000.0|
| 42|    3|  male|     Tom|  18000.0|
| 21|    3|female|  Kattie|  21000.0|
| 19|    2|  male|     Jen|   8000.0|
| 30|    2|female|     Jen|  28000.0|
| 42|    3|  male|     Tom|  18000.0|
| 18|    3|female|XiaoFang|  58000.0|
| 25|    1|  male|     Leo|20000.126|
+---+-----+------+--------+---------+
join

在SQL语言中很多场景需要用join操作,DataFrame中同样也提供了join的功能。 并且提供了六个重载的join方法。

using一个字段形式

下面这种join类似于a join b using column1的形式,需要两个DataFrame中有相同的一个列名。

val employeeDF2 = spark.read.json("hdfs://node01:9000/company/employee.json")
employeeDF.join(employeeDF2, "depId").show
+-----+---+------+------+---------+---+------+--------+---------+
|depId|age|gender|  name|   salary|age|gender|    name|   salary|
+-----+---+------+------+---------+---+------+--------+---------+
|    1| 25|  male|   Leo|20000.126| 35|  male|    Jack|  15000.0|
|    1| 25|  male|   Leo|20000.126| 25|  male|     Leo|20000.126|
|    2| 30|female| Marry|  25000.0| 30|female|     Jen|  28000.0|
|    2| 30|female| Marry|  25000.0| 19|  male|     Jen|   8000.0|
|    2| 30|female| Marry|  25000.0| 30|female|   Marry|  25000.0|
|    1| 35|  male|  Jack|  15000.0| 35|  male|    Jack|  15000.0|
|    1| 35|  male|  Jack|  15000.0| 25|  male|     Leo|20000.126|
|    3| 42|  male|   Tom|  18000.0| 18|female|XiaoFang|  58000.0|
|    3| 42|  male|   Tom|  18000.0| 42|  male|     Tom|  18000.0|
|    3| 42|  male|   Tom|  18000.0| 21|female|  Kattie|  21000.0|
|    3| 42|  male|   Tom|  18000.0| 42|  male|     Tom|  18000.0|
|    3| 21|female|Kattie|  21000.0| 18|female|XiaoFang|  58000.0|
|    3| 21|female|Kattie|  21000.0| 42|  male|     Tom|  18000.0|
|    3| 21|female|Kattie|  21000.0| 21|female|  Kattie|  21000.0|
|    3| 21|female|Kattie|  21000.0| 42|  male|     Tom|  18000.0|
|    2| 19|  male|   Jen|   8000.0| 30|female|     Jen|  28000.0|
|    2| 19|  male|   Jen|   8000.0| 19|  male|     Jen|   8000.0|
|    2| 19|  male|   Jen|   8000.0| 30|female|   Marry|  25000.0|
|    2| 30|female|   Jen|  28000.0| 30|female|     Jen|  28000.0|
|    2| 30|female|   Jen|  28000.0| 19|  male|     Jen|   8000.0|
+-----+---+------+------+---------+---+------+--------+---------+

using多个字段形式

除了上面这种using一个字段的情况外,还可以using多个字段,如下:

employeeDF.join(employeeDF2, Seq("name", "depId")).show
+--------+-----+---+------+---------+---+------+---------+
|    name|depId|age|gender|   salary|age|gender|   salary|
+--------+-----+---+------+---------+---+------+---------+
|     Leo|    1| 25|  male|20000.126| 25|  male|20000.126|
|   Marry|    2| 30|female|  25000.0| 30|female|  25000.0|
|    Jack|    1| 35|  male|  15000.0| 35|  male|  15000.0|
|     Tom|    3| 42|  male|  18000.0| 42|  male|  18000.0|
|     Tom|    3| 42|  male|  18000.0| 42|  male|  18000.0|
|  Kattie|    3| 21|female|  21000.0| 21|female|  21000.0|
|     Jen|    2| 19|  male|   8000.0| 30|female|  28000.0|
|     Jen|    2| 19|  male|   8000.0| 19|  male|   8000.0|
|     Jen|    2| 30|female|  28000.0| 30|female|  28000.0|
|     Jen|    2| 30|female|  28000.0| 19|  male|   8000.0|
|     Tom|    3| 42|  male|  18000.0| 42|  male|  18000.0|
|     Tom|    3| 42|  male|  18000.0| 42|  male|  18000.0|
|XiaoFang|    3| 18|female|  58000.0| 18|female|  58000.0|
+--------+-----+---+------+---------+---+------+---------+

指定join类型

在上面的using多个字段的join情况下,可以写第三个String类型参数,指定join的类型,如下所示:

inner:内连

outer,full,full_outer:全连

left, left_outer:左连

right,right_outer:右连

left_semi:过滤出joinDF1中和joinDF2共有的部分

left_anti:过滤出joinDF1中joinDF2没有的部分

employeeDF.join(employeeDF2, Seq("name", "depId"), "inner").show
+--------+-----+---+------+---------+---+------+---------+
|    name|depId|age|gender|   salary|age|gender|   salary|
+--------+-----+---+------+---------+---+------+---------+
|     Leo|    1| 25|  male|20000.126| 25|  male|20000.126|
|   Marry|    2| 30|female|  25000.0| 30|female|  25000.0|
|    Jack|    1| 35|  male|  15000.0| 35|  male|  15000.0|
|     Tom|    3| 42|  male|  18000.0| 42|  male|  18000.0|
|     Tom|    3| 42|  male|  18000.0| 42|  male|  18000.0|
|  Kattie|    3| 21|female|  21000.0| 21|female|  21000.0|
|     Jen|    2| 19|  male|   8000.0| 30|female|  28000.0|
|     Jen|    2| 19|  male|   8000.0| 19|  male|   8000.0|
|     Jen|    2| 30|female|  28000.0| 30|female|  28000.0|
|     Jen|    2| 30|female|  28000.0| 19|  male|   8000.0|
|     Tom|    3| 42|  male|  18000.0| 42|  male|  18000.0|
|     Tom|    3| 42|  male|  18000.0| 42|  male|  18000.0|
|XiaoFang|    3| 18|female|  58000.0| 18|female|  58000.0|
+--------+-----+---+------+---------+---+------+---------+

使用Column类型来join

如果不用using模式,灵活指定join字段的话,可以使用如下形式:

employeeDF.join(departmentDF, employeeDF("depId") === departmentDF("id")).show
+---+-----+------+--------+---------+---+--------------------+
|age|depId|gender|    name|   salary| id|                name|
+---+-----+------+--------+---------+---+--------------------+
| 25|    1|  male|     Leo|20000.126|  1|Technical Department|
| 30|    2|female|   Marry|  25000.0|  2|Financial Department|
| 35|    1|  male|    Jack|  15000.0|  1|Technical Department|
| 42|    3|  male|     Tom|  18000.0|  3|       HR Department|
| 21|    3|female|  Kattie|  21000.0|  3|       HR Department|
| 19|    2|  male|     Jen|   8000.0|  2|Financial Department|
| 30|    2|female|     Jen|  28000.0|  2|Financial Department|
| 42|    3|  male|     Tom|  18000.0|  3|       HR Department|
| 18|    3|female|XiaoFang|  58000.0|  3|       HR Department|
+---+-----+------+--------+---------+---+--------------------+

在指定join字段同时指定join类型

employeeDF.join(departmentDF, employeeDF("depId") === departmentDF("id"), "inner").show
+---+-----+------+--------+---------+---+--------------------+
|age|depId|gender|    name|   salary| id|                name|
+---+-----+------+--------+---------+---+--------------------+
| 25|    1|  male|     Leo|20000.126|  1|Technical Department|
| 30|    2|female|   Marry|  25000.0|  2|Financial Department|
| 35|    1|  male|    Jack|  15000.0|  1|Technical Department|
| 42|    3|  male|     Tom|  18000.0|  3|       HR Department|
| 21|    3|female|  Kattie|  21000.0|  3|       HR Department|
| 19|    2|  male|     Jen|   8000.0|  2|Financial Department|
| 30|    2|female|     Jen|  28000.0|  2|Financial Department|
| 42|    3|  male|     Tom|  18000.0|  3|       HR Department|
| 18|    3|female|XiaoFang|  58000.0|  3|       HR Department|
+---+-----+------+--------+---------+---+--------------------+
获取指定字段统计信息

stat方法可以用于计算指定字段或指定字段之间的统计信息,比如方差,协方差等。这个方法返回一个DataFramesStatFunctions类型对象。
下面代码演示根据字段,统计该字段值出现频率在30%以上的内容。在employeeDF中字段depId的内容为"1,2,3"。其中"1,2,3"出现频率均大于0.3。

employeeDF.stat.freqItems(Seq ("depId") , 0.3).show()
+---------------+
|depId_freqItems|
+---------------+
|      [2, 1, 3]|
+---------------+
获取两个DataFrame中共有的记录

intersect方法可以计算出两个DataFrame中相同的记录。

employeeDF.intersect(employeeDF.limit(1)).show()
+---+-----+------+----+---------+
|age|depId|gender|name|salary   |
+---+-----+------+----+---------+
|25 |1    |male  |Leo |20000.126|
+---+-----+------+----+---------+
获取一个DataFrame中有另一个DataFrame中没有的记录
employeeDF.except(employeeDF.limit(1)).show()
+---+-----+------+--------+-------+
|age|depId|gender|name    |salary |
+---+-----+------+--------+-------+
|42 |3    |male  |Tom     |18000.0|
|21 |3    |female|Kattie  |21000.0|
|19 |2    |male  |Jen     |8000.0 |
|30 |2    |female|Jen     |28000.0|
|35 |1    |male  |Jack    |15000.0|
|30 |2    |female|Marry   |25000.0|
|18 |3    |female|XiaoFang|58000.0|
+---+-----+------+--------+-------+
操作字段名

withColumnRenamed

如果指定的字段名不存在,不进行任何操作。下面示例中将employeeDF中的depId字段重命名为departmentId。

employeeDF.withColumnRenamed("depId",  "departmentId").show
+---+------------+------+--------+---------+
|age|departmentId|gender|    name|   salary|
+---+------------+------+--------+---------+
| 25|           1|  male|     Leo|20000.126|
| 30|           2|female|   Marry|  25000.0|
| 35|           1|  male|    Jack|  15000.0|
| 42|           3|  male|     Tom|  18000.0|
| 21|           3|female|  Kattie|  21000.0|
| 19|           2|  male|     Jen|   8000.0|
| 30|           2|female|     Jen|  28000.0|
| 42|           3|  male|     Tom|  18000.0|
| 18|           3|female|XiaoFang|  58000.0|
+---+------------+------+--------+---------+

withColumn

whtiColumn(colName: String , col: Column)方法根据指定colName往DataFrame中新增一列,如果colName已存在,则会覆盖当前列。

employeeDF.withColumn("salary2", employeeDF("salary")).show
+---+-----+------+--------+---------+---------+
|age|depId|gender|    name|   salary|  salary2|
+---+-----+------+--------+---------+---------+
| 25|    1|  male|     Leo|20000.126|20000.126|
| 30|    2|female|   Marry|  25000.0|  25000.0|
| 35|    1|  male|    Jack|  15000.0|  15000.0|
| 42|    3|  male|     Tom|  18000.0|  18000.0|
| 21|    3|female|  Kattie|  21000.0|  21000.0|
| 19|    2|  male|     Jen|   8000.0|   8000.0|
| 30|    2|female|     Jen|  28000.0|  28000.0|
| 42|    3|  male|     Tom|  18000.0|  18000.0|
| 18|    3|female|XiaoFang|  58000.0|  58000.0|
+---+-----+------+--------+---------+---------+
行转列

有时候需要根据某个字段内容进行分割,然后生成多行,这时可以使用explode方法 。
下面代码中,根据name字段中的空格将字段内容进行分割,分割的内容存储在新的字段name_中,如下所示:

departmentDF.explode("name", "name_"){x:String => x.split(" ")}.show
+---+--------------------+----------+
| id|                name|     name_|
+---+--------------------+----------+
|  1|Technical Department| Technical|
|  1|Technical Department|Department|
|  2|Financial Department| Financial|
|  2|Financial Department|Department|
|  3|       HR Department|        HR|
|  3|       HR Department|Department|
+---+--------------------+----------+

4.3 两种风格写法的说明

package com.qf.sql.day01

import org.apache.spark.sql.{DataFrame, Dataset, Row, SparkSession}


object _04SparkSqlExercise {
    def main(args: Array[String]): Unit = {
        val spark = SparkSession.builder().appName("test").master("local").getOrCreate()
        import spark.implicits._
        /**
         * DSL风格的写法:
         * Domain-specific language  :特殊领域的语言
         */
        val df: DataFrame = spark.read.json("data/emp.json")
        val value: Dataset[Row] = df.select($"empno", $"ename",$"deptno")
          .where("sal<3000 and deptno <30")
            .orderBy($"deptno".desc)
        value.show()

        /**
         * SQL风格的写法,说的就是SQl编程模型
         *
         * 注意:需要为读取到的数据维护一张表
         *
         *   df.createOrReplaceGlobalTempView():   global是全局的意思,表示整个spark程序中都可以访问到
         *   df.createGlobalTempView()                              没有global:仅当前任务中可以访问的
         *   df.createOrReplaceTempView()          replace:   有replace,表示如果存在就会替换,不存在也创建
         *   df.createTempView()                              无replace, 如果存在就报错,不存在就创建
         *
         *
         *   注意:建议使用  createOrReplaceTempView 或 createTempView
         *
         */
        df.createOrReplaceTempView("emp")
        val sql = "select deptno, count(*),max(sal),min(comm) from emp group by deptno"
        val frame: DataFrame = spark.sql(sql.stripMargin)
        frame.show()
        spark.stop()
    }
}

4.4 DataFrame和Dataset的创建方式

4.4.1 DataFrame的创建方式

1. 可以直接使用SparkSession.read获取DataFrameReader阅读器对象,然后调用相应的读取文件的方法来返回一
个Dataframe对象

2. 使用javaBean+反射机制,获取DataFrame对象,
	需要调用createDataFrame(data: java.util.List[_], beanClass: Class[_])

3. 使用动态编程方式:
	也就是要使用Row,StructType,StrunctField三种类型来描述,同时使用
	createDataFrame(rowRDD: RDD[Row], schema: StructType)
4. 从rdd转换而来:
	需要隐式导入:import spark.implicits._
	rdd.toDF()
	rdd.toDF(colNames: String*)  :用于指定rdd转成df时的列名

案例:

package com.qf.sql.day01

import org.apache.spark.rdd.RDD
import org.apache.spark.sql.types.{DataTypes, StructField, StructType}
import org.apache.spark.sql.{DataFrame, Row, SparkSession}

object _05DataFrameCreatemethod {
    /**
     * 方法3:使用动态编程
     *
     *  Row:代表的是二维表中的一行记录,相当于一个Java对象
     *  StructType:是该二维表的元数据信息,是StructField的集合
     *  StructField:是该二维表中某一个字段/列的元数据信息(主要包括,列名,类型,是否可以为null)
     * @param args
     */
    def main(args: Array[String]): Unit = {
        val spark = SparkSession.builder().appName("test").master("local").getOrCreate()
        //获取一个RDD对象,要使用SparkContext.   sparkSession对象里可以直接获取
        val rdd1: RDD[Row] = spark.sparkContext.parallelize(List(
            Row(1001, "zs", "m", 23),
            Row(1002, "ls", "m", 24),
            Row(1003, "ww", "m", 25),
            Row(1004, "zl", "m", 26),
            Row(1005, "xq", "m", 27)
        ))
        //描述RDD数据的元数据
        val schema = StructType(List(
            StructField("id",DataTypes.IntegerType,false),
            StructField("name",DataTypes.StringType,false),
            StructField("gender",DataTypes.StringType,false),
            StructField("age",DataTypes.IntegerType,false)
        ))
        val df: DataFrame = spark.createDataFrame(rdd1, schema)
        df.show()
        spark.stop()

    }

    /**
     * 方法1:就是使用DataFrameReader来读取文件
     * 方法2:使用javaBean+反射机制,获取DataFrame对象
     * @param args
     */
    def javaBean(args: Array[String]): Unit = {
        val spark = SparkSession.builder().appName("test").master("local").getOrCreate()
        import spark.implicits._

        //描述一个学生集合对象
        val students = List(new Student(1001,"zs","f",23),
            new Student(1002,"ls","m",23),
            new Student(1003,"ww","f",25),
            new Student(1004,"zl","f",26))

        import scala.collection.JavaConversions._
        //调用SparkSession的createDataFrame方法
        val df: DataFrame = spark.createDataFrame(students, classOf[Student])
        df.show()

        spark.stop()
    }
}

4.4.2 Dataset的创建方式

package com.qf.sql.day01

import org.apache.spark.sql.{Dataset, SparkSession}

/**
 * DataSet就是DataFrame的升级版
 *
 *
 *  spark.createDataset(List|Seq|Array)
 * 创建方式是一样的,都可以使用动态编程方式
 * 1. 参数是scala的集合对象
 * 2. 有一个隐式参数:需要导入spark的隐式方法
 * 3. 集合的元素类型是一个样例类
 */
object _06DataSetCreatemethod {
    def main(args: Array[String]): Unit = {
        val spark = SparkSession.builder().appName("test").master("local").getOrCreate()

        //维护一个普通集合
        val girls =List(
            Girl(1001, "zs", "m", 23),
            Girl(1002, "ls", "m", 24),
            Girl(1003, "ww", "m", 25),
            Girl(1004, "zl", "m", 26),
            Girl(1005, "xq", "m", 27)
        )
        import spark.implicits._
        val ds: Dataset[Girl] = spark.createDataset(girls)
        ds.show()
        spark.stop()
    }
}
case class Girl(id:Int,name:String,gender:String,age:Int)

4.5 RDD与DataFrame、Dataset之间的转换

4.5.1 RDD的转换

RDD–>DataFrame: 可以调用createDataFrame()
RDD->Dataset:可以调用createDataset()

--也可以直接调用相应方法转
比如:toDF(),toDS(),  需要隐式导入

4.5.2 DataFrame的转换

DataFrame->RDD    : df.rdd
DataFrame->Dataset :不能直接转,需要调用其他算子

4.5.3 Dataset的转换

Dataset->RDD    : ds.rdd
Dataset->DataFrame : ds.toDF()

五、sparksql的加载与落地

5.1 SparkSql的加载

1. 正规方法应该使用
	spark.read.load(path:String)
2. load方法默认加载的是parquet文件
3. 如果想要加载其他格式的文件,需要使用format(source:String)
   比如:
   spark.read.format("json").load(path:String)
   spark.read.format("csv").load(path:String)
   //想要获取csv的第一行作为表头
   spark.read.format("csv").option("header","true").load(path:String)
4. 可以读取数据库中的数据,可以使用jdbc方法
	spark.read.jdbc(url: String, table: String, properties: Properties)
package com.qf.sql.day02

import java.util.Properties

import org.apache.spark.sql.{DataFrame, SparkSession}

object _01SqlFileLoad {
    def main(args: Array[String]): Unit = {
        val spark: SparkSession = SparkSession.builder().appName("load").master("local").getOrCreate()
        //读取文件的方式:
        /**
         *   正规加载文件的方式是load方法,  load方法默认加载的是parquet存储格式的文件
         *
         *   如果想要读取其他格式的文件,需要使用format方法来指定具体格式
         */
        /*val df: DataFrame = spark.read.load("data/users.parquet")*/
       /* val df: DataFrame = spark.read.format("json").load("data/emp.json")*/
        /**
         *  加载csv文件时,默认使用的是逗号作为分隔符进行切分成多个字段的
         */
        /*val df: DataFrame = spark.read.format("csv").load("data/country.csv")*/
        // 这种读取不是我们想要的结果
        /*val df: DataFrame = spark.read.format("csv").load("data/ip-pprovince-count.csv")*/
        //需要指定分隔符进行切分. option("sep","指定分隔符")
        // 如果想要第一行作为表头需要使用option("header","true")
       /* val df: DataFrame = spark.read.format("csv").option("sep",";").option("header","true").load("data/ip-pprovince-count.csv")
        df.select("counts").filter("province=='北京'").show()*/
        /*val df: DataFrame = spark.read.format("orc").load("data/student.orc")*/
       // 纯文本文件,只能解析成一列,option("sep",",")是无效的
        /* val df: DataFrame = spark.read.format("text").option("sep",",").load("data/people.txt")
        df.show*/

        /*
         * 我们可以使用简化的方式来读取各种类型的数据源
         *  spark.read.json()
         *  spark.read.csv()
         *  spark.read.load()
         *  spark.read.orc()
         *  spark.read.text()
         */

        /*
         * 练习读取mysql中的数据
         */
        val prop = new Properties()
        prop.setProperty("user","root")
        prop.setProperty("password","123456")
        val df: DataFrame = spark.read.jdbc(
            "jdbc:mysql://localhost:3306/hz2003", "emp", prop)
        df.show()


        spark.stop()
    }
}

5.2 SparkSql的落地

1. 使用的方法是
	df.write.save(path:String) 
2. 默认写出的存储格式是parquet
3. 如果想要使用其他格式作为存储,需要使用format(source:String)
   如果是csv格式的存储,可以将表头写到第一行
   df.write.format("csv").option("header","true").save(path:String) 
4. 如果想要保存到数据库中
   df.write.jdbc(url: String, table: String, properties: Properties)
   注意存储模式:
    SaveMode.Append              :从后追加,注意主键问题
    SaveMode.Overwrite           :删除并重建
    SaveMode.ErrorIfExists       :如果存在就抛异常               默认模式
    SaveMode.Ignore              :如果数据存在,就不进行保存DataFrame的数据
     
   如果想要指定格式:
   df.write.mode(SaveMode.Append )
   .jdbc(url: String, table: String, properties: Properties)

六、SparkSql与hive的整合

SparkSQL和Hive的整合,是一种比较常见的关联处理方式,SparkSQL加载Hive中的数据进行业务处理,同时将计算结果落地回Hive中。

在这里,我们的目的是用SparkSql来读取Hive里的数据,而不是更换hive的执行引擎.

drop table emp;
create table emp(
empno int,
ename string,
job string,
mgr int,
hiredate string,
sal int,
comm int,
deptno int
)
row format delimited
fields terminated by ",";



select deptno,
max(sal),
min(sal),
round(avg(nvl(sal,0)),0)
from
emp
group by deptno
order by deptno desc

load data local inpath "/root/emp.txt" into table emp;





drop table emp1;
create table emp2(
empno int,
ename string,
job string,
mgr int,
hiredate string,
sal int,
comm int,
deptno int
)
partitioned by(dt string)
row format delimited
fields terminated by ","

注意事项:

在整合过程中,可能会遇到以下问题:
1. 权限问题
	方法1: hdfs dfs -chmod -R 777 /
	方法2: 在代码中使用System.setProperty("HADOOP_USER_NAME","root"),这一行要放在获取SparkSession对象之前
2. 落地到hive时,只有表结构,没有表数据。
	原因:idea在使用resources目录下的hive-site.xml这个文件时,没有将路径解析成HDFS上,而是解析成了本地的路径,将数据写在了本地,不是hdfs.
	解决办法:配置以下属性
	<property>
		<name>hive.metastore.warehouse.dir</name>
		<value>/user/hive/warehouse</value>
	</property>

七、SparkSql的函数

7.1 说明

1. Sparksql中的函数多数都是指其他编程语言中的函数,比如hive中的函数,在SparkSql中都可以使用。
2. 当然SparkSql也提供了自己的内置函数,如果想要使用内置函数,需要导入内置函数
   import org.apache.spark.sql.Functions._
3. 用户也可以自定义函数,比如UDF(一对一),UDAF(多对一,比如max),UDTF(一对多,比如explode)函数   
4. 函数的分类
	- 从功能上分类: 日期函数,数值,字符串函数,统计函数,特殊函数
	- 从自定义函数上(实现方式)分类:UDF,UDAF,UDTF。

7.2 hive函数的回顾

1. 查看内置函数,及其帮助信息
    hive> show functions;
    hive> desc function funcName

2. unix_timestamp() : 可以获取当前系统的时间戳,单位是秒, 返回的是本初子午线的时间戳
    hive (sz2003)> select unix_timestamp("1970-01-01 00:00:00");
    OK
3. 将时间戳转成日期:from_unixtime(unix_time, format)  注意:输入的是本初子午线的时间戳,返回的是东八区的日期
	hive> select from_unixtime(0,"yyyy-MM-dd HH:mm:ss");
4. to_date()  :将年月份时分秒的格式截取年月日   ,截取时非常好用
5. 六个时间分量函数:其实就是用于截取日期时间的六个部分
	year()
	month()
	day()
	hour()
	minute()
	second()
6. 
	select instr("helloworld","h");
 	instr(string,substring) :返回的是substring在string中的第一次遇到的第一个字符的位置。

开窗函数的:

package com.qf.sql.day03

import org.apache.spark.sql.{DataFrame, SparkSession}

object _01FunctionDemo01 {
    def main(args: Array[String]): Unit = {
        val spark: SparkSession = SparkSession.builder().appName("test").master("local").getOrCreate()
        val df: DataFrame = spark.read.json("data/emp.json")
        import spark.implicits._
        import org.apache.spark.sql.functions._
        //获取入职日期的分量
        df.select(year(to_date($"hiredate","yyyy-MM-dd")).as("year"),
            month(to_date($"hiredate","yyyy-MM-dd")).as("month"),
            dayofmonth(to_date($"hiredate","yyyy-MM-dd")).as("day")).show()

        /**
         * 创建临时表
         */
        df.createTempView("emp")
        spark.sql(
            """
              |select distinct deptno,
              |count(1) over(partition by deptno order by deptno)
              |from emp
              |""".stripMargin).show()
        spark.stop()

    }
}

7.3 用户自定义函数

1)UDF案例1

package com.qf.sql.day02

import org.apache.spark.sql.{DataFrame, SparkSession}

/**
 * 用户自定义函数的应用
 * 1. 定义一个函数:
 * 2. 注册函数:
 *    spark.udf.register(name:String,func:Function)
 */
object _05UserDefineFunction {
    def main(args: Array[String]): Unit = {
        val spark = SparkSession.builder().master("local").appName("test").getOrCreate()
        val df: DataFrame = spark.read.json("file:///D:\\Users\\Michael\\Documents\\IdeaProjects\\sz2003\\sz2003_sparksql01/data/emp.json")
        /**
         * 换SQL风格
         */
        //维护一个临时表
        df.createTempView("emp")

        //定义一个函数:
        val getLength=(str:String)=>str.length
        //注册函数
        spark.udf.register("mylength",getLength)

        val sql=
            """
              |select ename,
              |mylength(ename)
              |from
              |emp
              |""".stripMargin
        spark.sql(sql).show()
        spark.stop()
    }
}

2)UDF案例2

package com.qf.sql.day02

import org.apache.spark.sql.{DataFrame, SparkSession}

object _06UserDefineFunction {
    def main(args: Array[String]): Unit = {
        val spark = SparkSession.builder().master("local").appName("test").getOrCreate()
        val df: DataFrame = spark.read.json("file:///D:\\Users\\Michael\\Documents\\IdeaProjects\\sz2003\\sz2003_sparksql01/data/emp.json")

        //维护一张临时奥b
        df.createTempView("emp")
        //注册函数
        spark.udf.register("getLevel",getLevel _)


       /* val sql =
            """
              |select ename,
              |job,
              |sal,
              |case when sal>3000 then 'level1'
              |when sal>1500 then 'level2'
              |else 'level3' end as level
              |from
              |emp
              |""".stripMargin*/
       val sql =
           """
             |select ename,
             |job,
             |sal,
             |getLevel(sal) as level
             |from
             |emp
             |""".stripMargin
        spark.sql(sql).show()
        spark.stop()
    }
    def getLevel(sal:Int)={
        if(sal>3000){
            "level1"
        }else if(sal>1500){
            "level2"
        }else{
            "level3"
        }
    }
}

3)UDAF案例演示

package com.qf.sql.day02

import org.apache.spark.rdd.RDD
import org.apache.spark.sql.expressions.{MutableAggregationBuffer, UserDefinedAggregateFunction}
import org.apache.spark.sql.types.{DataType, DataTypes, StructField, StructType}
import org.apache.spark.sql.{Dataset, Row, SparkSession}

/**
 * 用户自定义分析函数UDAF案例演示:
 *    自定义一个求平均值的函数:   多对一。
 * @param id
 * @param name
 * @param score
 */

case class Score(id:Int,name:String,score:Double)

object Test04 {
    def main(args: Array[String]): Unit = {
        val spark = SparkSession.builder().appName("udaf").master("local").getOrCreate()
        val list = List(
            Score(1,"张三",99.9),
            Score(2,"李四",88.9),
            Score(3,"小明",77.9),
            Score(1,"张三",91.9),
            Score(2,"李四",81.9),
            Score(3,"小明",71.9)
        )

        import spark.implicits._
        //scala的对象可以直接转DS或者是DF, 当然需要导入隐式转换
        val ds: Dataset[Score] = list.toDS()
        /*维护一张表*/
        ds.createTempView("tmp")
        //注册函数:
        spark.udf.register("myavg",new MyAvgUDAF)

        val sql = "select avg(score), myavg(score),name from tmp group by name"
        spark.sql(sql).show()
    }
}

/**
 * 1:需要继承 UserDefinedAggregateFunction
 * 2:重写方法
 */
class  MyAvgUDAF extends  UserDefinedAggregateFunction{
    /**
     *  指定用户自定义udaf输入参数的元数据
     * @return
     */
    override def inputSchema: StructType = {
        StructType(Array(StructField("score",DataTypes.DoubleType)))
    }

    /**
     * udaf自定义函数求解过程中的临时变量的数据类型
     *
     * 因为要求平均值,正常逻辑是求出总和以及个数,然后做除法运算,因此要有两个临时变量
     * @return
     */
    override def bufferSchema: StructType = {
        StructType(Array(
            StructField("sum",DataTypes.DoubleType),
            StructField("count",DataTypes.IntegerType)
        ))
    }

    /**
     * udaf返回值的数据类型
     * @return
     */
    override def dataType: DataType = DataTypes.DoubleType

    override def deterministic: Boolean = true

    /**
     * 临时变量是存储在buffer中,我们定义的buffer有两个元素,第一元素是sum,第二个元素是count
     * @param buffer
     */
    override def initialize(buffer: MutableAggregationBuffer): Unit = {
        buffer.update(0,0d) //设置第一个元素的的初始值为0d
        buffer.update(1,0) // 设置第二个元素的初始值为0
    }

    /**
     * 分区内的局部累加操作
     * @param buffer
     * @param input
     */
    override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
        val score: Double = input.getAs[Double](0)
        buffer.update(0,buffer.getDouble(0)+score) //将新进来的行中的分数累加到第一个元素sum上
        buffer.update(1,buffer.getInt(1)+1)        //将第二个元素count累加一个1
    }

    /**
     * 分区间的累加操作
     * @param buffer1
     * @param buffer2
     */
    override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
        buffer1.update(0,buffer1.getDouble(0)+buffer2.getDouble(0))
        buffer1.update(1,buffer1.getInt(1)+buffer2.getInt(1))
    }
    /**
     * 用于计算输出结果
     * @param buffer
     * @return
     */
    override def evaluate(buffer: Row): Any = buffer.getDouble(0)/buffer.getInt(1)
}

八、SparkSql的优化

1. 可以将df或者是ds进行缓存:
	df.cache   :  默认存储级别是:memory_and_disk
	df.persist
	
	底层是将df的数据缓存成柱状结构,在查询时,可以指定字段进行柱状扫描,提高查询效率
2. 调优参数:

3. 一些基本原则(九大原则):
	参考:https://tech.meituan.com/2016/04/29/spark-tuning-basic.html
4. 数据倾斜的优化:
	参考:https://tech.meituan.com/2016/05/12/spark-tuning-pro.html

	什么叫数据倾斜:一个分区的数据要远远大于其他的分区的数据,那么整个应用程序的执行完成时间与这个分区处理的时间有关系,而且可能造成应用程序作业失败,或者是卡住不动了。
	因此出现数据倾斜的时候,Spark作业看起来会运行得非常缓慢,甚至可能因为某个task处理的数据量过大导致内存溢出。

问题分析:具体问题具体分析
- 如何定位导致数据倾斜的代码?
	数据倾斜只会发生在shuffle过程中。这里给大家罗列一些常用的并且可能会触发shuffle操作的算子:
	distinct、groupByKey、reduceByKey、aggregateByKey、join、cogroup、repartition等。出现数据
	倾斜时,可能就是你的代码中使用了这些算子中的某一个所导致的。
- 某个task执行特别慢的情况	
	首先要看的,就是数据倾斜发生在第几个stage中。
	(1)yarn-client模式提交,那么本地是直接可以看到log的,可以在log中找到当前运行到了第几个stage
	(2)yarn-cluster模式提交,则可以通过Spark Web UI来查看
- 某个task莫名其妙内存溢出的情况:
	查看log中的异常
- 查看导致数据倾斜的key的数据分布情况
   知道了数据倾斜发生在哪里之后,通常需要分析一下那个执行了shuffle操作并且导致了数据倾斜的RDD/Hive表,查看一下其中key的分布情况。
   
   此时根据你执行操作的情况不同,可以有很多种查看key分布的方式: 
   1. 如果是Spark SQL中的group by、join语句导致的数据倾斜,那么就查询一下SQL中使用的表的key分布情况。 
   2. 如果是对Spark RDD执行shuffle算子导致的数据倾斜,那么可以在Spark作业中加入查看key分布的代码,比如RDD.countByKey()。然后对统计出来的各个key出现的次数,collect/take到客户端打印一下,就可以看到key的分布情况。  

数据倾斜案例解决方案演示:

package com.qf.sql.day03

import org.apache.spark.sql.{DataFrame, SparkSession}
/**
 * 案例演示:wordcount的统计,出现数据倾斜的解决方案- 局部聚合+全局聚合
 */
object _02DataSkewDemo01 {
    def main(args: Array[String]): Unit = {
        val spark: SparkSession = SparkSession.builder().appName("skew").master("local").getOrCreate()
        import spark.implicits._

        //获取一个scala集合对象
        val list = List("a a a a a a a a a a a a a a a b b e c c e d f f","a a a b e f g c")
        val df: DataFrame = list.toDF("line")
        /*维护一个临时表*/
        df.createTempView("temp")

        /*分组统计每个单词的数量,可能会出现某一个单词过多的组*/
        val sql =
            """
              |select A.word,count(1)
              |from
              |(select
              |explode(split(line," ")) word
              |from temp
              |)A
              |group by A.word
              |""".stripMargin
        spark.sql(sql).show()


        print("----先随机数字,作为前缀,添加到单词的前面-----")
        val randSql =
            """
              |select A.word,concat(floor(rand()*3),"_",A.word)
              |from
              |(
              |select
              |explode(split(line," ")) word
              |from temp
              |) A
              |""".stripMargin
        spark.sql(randSql).show()
        print("----局部聚合: 将同一个key加上前缀,产生多个不同的key,然后相同前缀的key进行局部聚合-----")
        val partSql =
            """
              |select B.prefix_word,count(1) prefix_count
              |from
              |(
              |select A.word,concat(floor(rand()*3),"_",A.word) prefix_word
              |from
              |(
              |select
              |explode(split(line," ")) word
              |from temp
              |) A
              |) B
              |group by B.prefix_word
              |""".stripMargin
        spark.sql(partSql).show()
        println()
        print("----去掉前缀,在完成全局聚合----")
        val fullSql =
            """
              |select substr(prefix_word,instr(prefix_word,"_")+1) f_word, sum(prefix_count)
              |from
              |(
              |select B.prefix_word,count(1) prefix_count
              |from
              |(
              |select A.word,concat(floor(rand()*3),"_",A.word) prefix_word
              |from
              |(
              |select
              |explode(split(line," ")) word
              |from temp
              |) A
              |) B
              |group by B.prefix_word
              |) C
              |group by f_word
              |""".stripMargin

        spark.sql(fullSql).show()
        spark.stop()
    }
}

九、SparkSql的运行架构

SparkSql执行的原理和数据库的sql类似,也使用到了词法/语法解析、绑定、优化、执行。

步骤如下:
1. 先进行词法/语法的校验: 语法是否正确,字段,表名是否存在等
   校验成功后,会生成语法树(B+tree)
2. 在使用RuleExcutor将一些规则绑定到树的节点,生成逻辑计划。
3. 优化器在对逻辑计划进行优化工作,比如内存的优化,磁盘的优化
4. 优化后,会转成相应的JOB,然后开始执行(底层用到了sparkCode的DAGScheduler等)

Spark SQL由Core、Catalyst、Hive、Hive-ThriftServer四部分构成:

Core: 负责处理数据的输入和输出,如获取数据,查询结果输出成DataFrame等
Catalyst: 负责处理整个查询过程,包括解析、绑定、优化等
Hive: 负责对Hive数据进行处理
Hive-ThriftServer: 主要用于对hive的访问

​ Hive-ThriftServer: 主要用于对hive的访问

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KmHBfAzJ-1615727684989)(ClassNotes.assets/optimizer-1.png)]

LogicalPlan–>逻辑计划 Analyzer–>分析器 Optimizer–>优化器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6FGjFfk3-1615727684991)(ClassNotes.assets/optimizer-2.png)]

unresolved:未实现的含义   
resolved:实现的含义
 """
          |select B.prefix_word,count(1) prefix_count
          |from
          |(
          |select A.word,concat(floor(rand()*3),"_",A.word) prefix_word
          |from
          |(
          |select
          |explode(split(line," ")) word
          |from temp
          |) A
          |) B
          |group by B.prefix_word
          |""".stripMargin
    spark.sql(partSql).show()
    println()
    print("----去掉前缀,在完成全局聚合----")
    val fullSql =
        """
          |select substr(prefix_word,instr(prefix_word,"_")+1) f_word, sum(prefix_count)
          |from
          |(
          |select B.prefix_word,count(1) prefix_count
          |from
          |(
          |select A.word,concat(floor(rand()*3),"_",A.word) prefix_word
          |from
          |(
          |select
          |explode(split(line," ")) word
          |from temp
          |) A
          |) B
          |group by B.prefix_word
          |) C
          |group by f_word
          |""".stripMargin

    spark.sql(fullSql).show()
    spark.stop()
}

}


# 九、SparkSql的运行架构

SparkSql执行的原理和数据库的sql类似,也使用到了词法/语法解析、绑定、优化、执行。

步骤如下:

  1. 先进行词法/语法的校验: 语法是否正确,字段,表名是否存在等
    校验成功后,会生成语法树(B+tree)
  2. 在使用RuleExcutor将一些规则绑定到树的节点,生成逻辑计划。
  3. 优化器在对逻辑计划进行优化工作,比如内存的优化,磁盘的优化
  4. 优化后,会转成相应的JOB,然后开始执行(底层用到了sparkCode的DAGScheduler等)

Spark SQL由Core、Catalyst、Hive、Hive-ThriftServer四部分构成:

Core: 负责处理数据的输入和输出,如获取数据,查询结果输出成DataFrame等
Catalyst: 负责处理整个查询过程,包括解析、绑定、优化等
Hive: 负责对Hive数据进行处理
Hive-ThriftServer: 主要用于对hive的访问


​	Hive-ThriftServer: 主要用于对hive的访问

[外链图片转存中...(img-KmHBfAzJ-1615727684989)]

LogicalPlan-->逻辑计划      Analyzer-->分析器     Optimizer-->优化器

[外链图片转存中...(img-6FGjFfk3-1615727684991)]

unresolved:未实现的含义
resolved:实现的含义


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值