Spark:实时数据微批处理(4.Spark sql及项目实战)

1.Spark SQL 概述

1.1 什么是 Spark SQL?

Spark SQL 是 Spark 用于结构化数据(structured data)处理的 Spark 模块

MapReduce -> hive-sql 为了解决mr程序编程困难。Hive SQL 转换成 MapReduce提交到集群上执行,执行效率慢

spark-core -> spark-sql 为了解决rdd编程困难。Spark SQL 转换成 RDD提交到集群上执行,执行效率非常快

Spark SQL 它提供了2个编程抽象, 类似 Spark Core 中的 RDD

  • DataFrame
  • DataSet

1.2 Spark SQL 的特点

  1. Integrated(易整合)
    无缝的整合了 SQL 查询和 Spark 编程.
    在这里插入图片描述

  2. Uniform Data Access(统一的数据访问方式)
    使用相同的方式连接不同的数据源.
    在这里插入图片描述

  3. Hive Integration(集成 Hive)
    在已有的仓库上直接运行 SQL 或者 HiveQL
    在这里插入图片描述

  4. Standard Connectivity(标准的连接方式)
    通过 JDBC 或者 ODBC 来连接
    在这里插入图片描述

1.3 DataFrame介绍

  1. 与 RDD 类似,DataFrame 也是一个分布式数据容器
  2. DataFrame 像传统数据库的二维表格,除了数据以外,还记录数据的结构信息,即schema
  3. DataFrame也支持嵌套数据类型(struct、array和map)
  4. DataFrame和RDD的区别:
    在这里插入图片描述
  • 左侧的RDD[Person]虽然以Person为类型参数,但Spark框架本身不了解Person类的内部结构。而右侧的DataFrame却提供了详细的结构信息,使得 Spark SQL 可以清楚地知道该数据集中包含哪些列,每列的名称和类型各是什么。
  • DataFrame是为数据提供了Schema的视图。可以把它当做数据库中的一张表来对待,
  • DataFrame也是懒执行的,性能上比 RDD要高,主要原因: 优化的执行计划:查询计划通过Spark catalyst optimiser进行优化

1.4 DataSet的介绍

  1. DataSet是DataFrame API的一个扩展,是 SparkSQL 最新的数据抽象(1.6新增)。
  2. 用户友好的API风格,既具有类型安全检查也具有DataFrame的查询优化特性。
  3. Dataset支持编解码器,当需要访问非堆上的数据时可以避免反序列化整个对象,提高了效率。
  4. 样例类被用来在DataSet中定义数据的结构信息,样例类中每个属性的名称直接映射到DataSet中的字段名称。
  5. DataFrame是DataSet的特列,DataFrame=DataSet[Row] ,所以可以通过as方法将DataFrame转换为DataSet。Row是一个类型,跟Car、Person这些的类型一样,所有的表结构信息都用Row来表示。
  6. DataSet是强类型的。比如可以有DataSet[Car],DataSet[Person].
  7. DataFrame只是知道字段,但是不知道字段的类型,所以在执行这些操作的时候是没办法在编译的时候检查是否类型失败的,比如你可以对一个String进行减法操作,在执行的时候才报错,而DataSet不仅仅知道字段,而且知道字段类型,所以有更严格的错误检查。就跟JSON对象和类对象之间的类比。

2.Spark SQL 编程

2.1 SparkSession 介绍

  • 在2.0版本以前,SparkSQL 提供两种 SQL 查询起始点:一个叫SQLContext,用于Spark 自己提供的 SQL 查询;一个叫 HiveContext,用于连接 Hive 的查询。
  • 从2.0开始, SparkSession是 Spark 最新的 SQL 查询起始点,实质上是SQLContext和HiveContext的组合,所以在SQLContext和HiveContext上可用的 API 在SparkSession上同样是可以使用的。
  • SparkSession内部封装了SparkContext,所以计算实际上是由SparkContext完成的。
  • 当我们使用 spark-shell 的时候, spark 会自动的创建一个叫做spark的SparkSession, 类似自动获取到一个sc来表示SparkContext

2.2 使用 DataFrame 进行编程

三种方式创建DF

  • 通过spark数据源创建(json, jdbc, csv,…)(数据源)
  • 从已知的rdd转换过来
  • 通过专门查询hive表创建

2.2.1 通过 Spark 数据源创建DF

Spark支持的数据源:
在这里插入图片描述

// 读取 json 文件
scala> val df = spark.read.json("/opt/module/spark-local/examples/src/main/resources/employees.json")
df: org.apache.spark.sql.DataFrame = [name: string, salary: bigint]

// 展示结果
scala> df.show
+-------+------+
|   name|salary|
+-------+------+
|Michael|  3000|
|   Andy|  4500|
| Justin|  3500|
|  Berta|  4000|
+-------+------+

2.2.2 通过 RDD 进行转换

2.2.3 通过查询 Hive 表创建

2.3 DataFrame 语法风格

2.3.1 SQL 语法风格(主要)

SQL 语法风格是指我们查询数据的时候使用 SQL 语句来查询.
这种风格的查询必须要有临时视图或者全局视图来辅助

scala> val df = spark.read.json("/opt/module/spark-local/examples/src/main/resources/people.json")
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]

scala> df.createOrReplaceTempView("people")

scala> spark.sql("select * from people").show
+----+-------+
| age|   name|
+----+-------+
|null|Michael|
|  30|   Andy|
|  19| Justin|
+----+-------+

注意:
1.临时视图只能在当前 Session 有效, 在新的 Session 中无效.
2.想在新的Session中生效,可以创建全局视图. 访问全局视图需要全路径:如global_temp.xxx

scala> val df = spark.read.json("/opt/module/spark-local/examples/src/main/resources/people.json")
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]

scala> df.createGlobalTempView("people")

scala> spark.sql("select * from global_temp.people")
res31: org.apache.spark.sql.DataFrame = [age: bigint, name: string]

scala> res31.show
+----+-------+
| age|   name|
+----+-------+
|null|Michael|
|  30|   Andy|
|  19| Justin|
+----+-------+


scala> spark.newSession.sql("select * from global_temp.people")
res33: org.apache.spark.sql.DataFrame = [age: bigint, name: string]

scala> res33.show
+----+-------+
| age|   name|
+----+-------+
|null|Michael|
|  30|   Andy|
|  19| Justin|
+----+-------+

2.3.2 DSL 语法风格

DataFrame提供一个特定领域语言(domain-specific language, DSL)去管理结构化的数据. 可以在 Scala, Java, Python 和 R 中使用 DSL
使用 DSL 语法风格不必去创建临时视图了

  • 查看 Schema 信息
scala> val df = spark.read.json("/opt/module/spark-local/examples/src/main/resources/people.json")
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]

scala> df.printSchema
root
|-- age: long (nullable = true)
|-- name: string (nullable = true)
  • 使用DSL查询

1.只查询name列数据
scala> df.select($"name").show
+-------+
|   name|
+-------+
|Michael|
|   Andy|
| Justin|
+-------+

scala> df.select("name").show
+-------+
|   name|
+-------+
|Michael|
|   Andy|
| Justin|
+-------+

2.查询name和age
scala> df.select("name", "age").show
+-------+----+
|   name| age|
+-------+----+
|Michael|null|
|   Andy|  30|
| Justin|  19|
+-------+----+

3.查询name和age + 1
scala> df.select($"name", $"age" + 1).show
+-------+---------+
|   name|(age + 1)|
+-------+---------+
|Michael|     null|
|   Andy|       31|
| Justin|       20|
+-------+---------+

注意:
4.设计到运算的时候, 每列都必须使用$

5.查询age大于20的数据
scala> df.filter($"age" > 21).show
+---+----+
|age|name|
+---+----+
| 30|Andy|
+---+----+

6.按照age分组,查看数据条数
scala> df.groupBy("age").count.show
+----+-----+
| age|count|
+----+-----+
|  19|    1|
|null|    1|
|  30|    1|
+----+-----+

2.4 RDD 和 DataFrame 的交互

2.4.1 RDD 到 DataFrame

1. 手动转换

import org.apache.spark.sql.SparkSession

/**
 * @Author jaffe
 * @Date 2020/05/12  16:25
 */
object RDD2DF {
  def main(args: Array[String]): Unit = {
    // 创建 SparkSession
    val spark = SparkSession.builder()
      .master("local[2]")
      .appName("RDD2DD")
      .getOrCreate()

    import spark.implicits._ //表示 SparkSession 的对象的隐式转换,用于实现rdd.toDF
    
    //得到df, 使用df编程
    //val df = spark.read.json("E:\\bigdata\\text\\input\\spark\\sql\\person.json")
    //df.createOrReplaceTempView("p")
    //spark.sql("select * from p").show

    // 把rdd转成df
    val rdd = spark.sparkContext.parallelize(List(("a", 1), ("b", 2), ("c", 3)))
    val result = rdd.toDF("name", "age")

    result.show()
    spark.close()

  }
}

2.通过样例类转换

import org.apache.spark.sql.SparkSession

/**
 * @Author jaffe
 * @Date 2020/05/12  16:42
 */
object RDD2DF_2 {
  def main(args: Array[String]): Unit = {

    val spark = SparkSession.builder()
      .master("local[2]")
      .appName("RDD2DD")
      .getOrCreate()

    import spark.implicits._

    val list = User(10, "a") :: User(20, "b") :: User(30, "c") :: User(40, "d") :: Nil
    val rdd = spark.sparkContext.parallelize(list)

    val f = rdd.toDF
    f.show()

    spark.close()
  }
}

case class User(age: Int, name: String)

2.4.2 DataFrame到RDD

直接调用DataFrame的rdd方法就完成了从转换

import org.apache.spark.sql.SparkSession

/**
 * @Author jaffe
 * @Date 2020/05/12  18:53
 */
object DF2RDD {
  def main(args: Array[String]): Unit = {
    //1创建SparkSession
    val spark = SparkSession.builder()
      .master("local[2]")
      .appName("DF2RDD")
      .getOrCreate()

    val df = spark.read.json("E:\\bigdata\\text\\input\\spark\\sql\\person.json")
    df.printSchema()

    // 从df转成rdd之后, rdd内存储的永远是Row
    // Row当成一个集合, 表示一行数据, 弱类型(类型与jdbc中的ResultSet)

    val rdd = df.rdd

    val rdd2 = rdd.map(row => {
      // 方式1
      // val age = if(row.isNullAt(0)) -1 else row.getLong(0)
      // val name = row.getString(1)

      //方式2
      val age = row.getAs[Long]("age")
      val name = row.getAs[String]("name")

      //方式3
      // val age = row.get(0)
      // val name = row.get(1)
      (age, name)
    })

    rdd2.collect.foreach(println)
    spark.close()

  }
}

2.5 使用 DataSet 进行编程

DataSet是具有强类型的数据集合,更加安全,需要提供对应的类型信息。

import org.apache.spark.sql.SparkSession

/**
 * @Author jaffe
 * @Date 2020/05/14  09:04
 */
object CreateDateSet {
  def main(args: Array[String]): Unit = {

    val spark = SparkSession.builder()
      .master("local[2]")
      .appName("CreateDateSet")
      .getOrCreate()

    import spark.implicits._

   /*
   1.使用基本类型的序列得到 DataSet
    val list = List(20,30,40,50,60)
    val ds = list.toDS
    ds.createOrReplaceTempView("a")
    ds.filter(_>30).show()
    ds.filter("value > 30").show() //dsl
    spark.sql("select * from a where value > 30").show()*/

    // 2. 使用样例类的序列得到DataSet
    val list1 = User(10, "a") :: User(20, "b") :: User(30, "c") :: User(40, "d") :: Nil
    val ds1 = list1.toDS()
    ds1.createOrReplaceTempView("b")
    spark.sql("select * from b where age > 20").show()
    ds1.filter(_.age>20).show()

  }
}

case class User(age: Int, name: String)

2.6 RDD 和 DataSet 的交互

2.6.1 RDD 到 DataSet

直接调用 toDS

import org.apache.spark.sql.SparkSession

/**
 * @Author jaffe
 * @Date 2020/05/14  09:28
 */
object RDD2DS {
  def main(args: Array[String]): Unit = {
    val spark = SparkSession.builder()
      .master("local[2]")
      .appName("RDD2DS")
      .getOrCreate()

    import spark.implicits._
    val list1 = User(10, "a") :: User(20, "b") :: User(30, "c") :: User(40, "d") :: Nil
    val rdd = spark.sparkContext.parallelize(list1)
    val ds = rdd.toDS()
    ds.show()

  }
}
case class User(age: Int, name: String)

2.6.2 DataSet 到 RDD

调用rdd方法即可

import org.apache.spark.sql.SparkSession

/**
 * @Author jaffe
 * @Date 2020/05/14  09:32
 */
object DS2RDD {
  def main(args: Array[String]): Unit = {

    val spark = SparkSession.builder()
      .master("local[2]")
      .appName("RDD2DS")
      .getOrCreate()

    import spark.implicits._
    val list1 = User(10, "a") :: User(20, "b") :: User(30, "c") :: User(40, "d") :: Nil
    val rdd = spark.sparkContext.parallelize(list1)
    val ds = rdd.toDS()
    ds.show()

    //转成RDD
    val rdd1 = ds.rdd
    rdd1.collect.foreach(println)

    spark.close()
  }
}

case class Use(age: Int, name: String)

2.7 DataFrame 和 DataSet 之间的交互

df -> ds

  • 有样例类
  • df.as[样例类]

ds -> df

  • ds.toDF
import org.apache.spark.sql.SparkSession

/**
 * @Author jaffe
 * @Date 2020/05/14  10:01
 */
object DF2DS {
  def main(args: Array[String]): Unit = {

    val spark = SparkSession.builder()
      .master("local[2]")
      .appName("DF2DS")
      .getOrCreate()

    import spark.implicits._
    // df -> ds  (简单->复杂)
    val df = spark.read.json("E:\\bigdata\\text\\input\\spark\\sql\\person.json")
    val ds = df.as[Person]
    ds.show()

    // ds -> df
    val df1 = ds.toDF()
    df1.show()

    spark.close()

  }
}

case class Person(name: String, age: Long)

2.8 RDD, DataFrame和 DataSet 之间的关系

版本发展:
RDD (Spark1.0) —> Dataframe(Spark1.3) —> Dataset(Spark1.6)

2.8.1 三者的共性

  1. RDD、DataFrame、Dataset全都是 Spark 平台下的分布式弹性数据集,为处理超大型数据提供便利
  2. 三者都有惰性机制,在进行创建、转换,如map方法时,不会立即执行,只有在遇到Action如foreach时,三者才会开始遍历运算。
  3. 三者都会根据 Spark 的内存情况自动缓存运算,这样即使数据量很大,也不用担心会内存溢出
  4. 三者都有partition的概念
  5. 三者有许多共同的函数,如map, filter,排序等
  6. 在对 DataFrame和Dataset进行操作许多操作都需要这个包进行支持 import spark.implicits._
  7. DataFrame和Dataset均可使用模式匹配获取各个字段的值和类型

2.8.2 三者的区别

  1. RDD:
1.RDD一般和spark mlib同时使用
2.RDD不支持sparksql操作
  1. DataFrame
1.与RDD和Dataset不同,DataFrame每一行的类型固定为Row,每一列的值没法直接访问,只有通过解析才能获取各个字段的值,
2.DataFrame与DataSet一般不与 spark mlib 同时使用
3.DataFrame与DataSet均支持 SparkSQL 的操作,比如select,groupby之类,还能注册临时表/视窗,进行 sql 语句操作
4.DataFrame与DataSet支持一些特别方便的保存方式,比如保存成csv,可以带上表头,这样每一列的字段名一目了然
  1. DataSet
1.Dataset和DataFrame拥有完全相同的成员函数,区别只是每一行的数据类型不同。 DataFrame其实就是DataSet的一个特例
2.DataFrame也可以叫Dataset[Row],每一行的类型是Row,不解析,每一行究竟有哪些字段,各个字段又是什么类型都无从得知,只能用上面提到的getAS方法或者共性中的第七条提到的模式匹配拿出特定字段。而Dataset中,每一行是什么类型是不一定的,在自定义了case class之后可以很自由的获得每一行的信息

2.8.3 三者的互相转换

在这里插入图片描述

2.9 自定义 SparkSQL 函数

2.9.1 spark-shell 自定义 UDF 函数

scala> val df = spark.read.json("examples/src/main/resources/people.json")
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]

scala> df.show
+----+-------+
| age|   name|
+----+-------+
|null|Michael|
|  30|   Andy|
|  19| Justin|
+----+-------+
// 注册一个 udf 函数: toUpper是函数名, 第二个参数是函数的具体实现
scala> spark.udf.register("toUpper", (s: String) => s.toUpperCase)
res1: org.apache.spark.sql.expressions.UserDefinedFunction = UserDefinedFunction(<function1>,StringType,Some(List(StringType)))

scala> df.createOrReplaceTempView("people")

scala> spark.sql("select toUpper(name), age from people").show
+-----------------+----+
|UDF:toUpper(name)| age|
+-----------------+----+
|          MICHAEL|null|
|             ANDY|  30|
|           JUSTIN|  19|
+-----------------+----+

2.9.2 用户自定义聚合函数 UDAF_SUM

import org.apache.spark.sql.{Row, SparkSession}
import org.apache.spark.sql.expressions.{MutableAggregationBuffer, UserDefinedAggregateFunction}
import org.apache.spark.sql.types.{DataType, DoubleType, StructField, StructType}

/**
 * @Author jaffe
 * @Date 2020/05/14  11:20
 */
object UDAF_SUM {
  def main(args: Array[String]): Unit = {
    val spark = SparkSession.builder()
      .master("local[2]")
      .appName("UDAF_SUM")
      .getOrCreate()

    import spark.implicits._
    spark.udf.register("my_sum", new MySum)

    val df = spark.read.json("E:\\bigdata\\text\\input\\spark\\sql\\person.json")
    df.createOrReplaceTempView("p")
    spark.sql("select my_sum(age) from p").show()

    spark.close()
    
  }
}

//UDAF
class MySum extends UserDefinedAggregateFunction { //用户定义聚合函数
  //输入的数据类型 StructType 可以封装多个  Double
  override def inputSchema: StructType = StructType(StructField("ele", DoubleType) :: Nil)

  //缓冲区的数据类型 Double
  override def bufferSchema: StructType = StructType(StructField("sum", DoubleType) :: Nil)

  //最终聚合的返回值结果类型 Double
  override def dataType: DataType = DoubleType

  //确定性,相同的输入是否返回相同的输出  相同的输入有相同的输出
  override def deterministic: Boolean = true

  //初始化:对缓冲区初始化
  override def initialize(buffer: MutableAggregationBuffer): Unit = {
    //缓冲区可以看成一个集合,当前只缓冲一个值 buffer(0)
    buffer(0) = 0D
  }

  //分区内聚合  参数1:缓冲区 参数2:传过来的每行数据  行的列数由传给聚合函数的参数来定
  override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
    //每个row中传一个值过来
    // 有个bug TODO
    buffer(0) = buffer.getDouble(0) + input.getDouble(0)
  }

  //分区间聚合 把buffer2中的数据,聚合到buffer1中
  override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
    //分区间的聚合一定是每个分区内聚合好的
    buffer1(0) = buffer1.getDouble(0) + buffer2.getDouble(0)
  }

  override def evaluate(buffer: Row): Any = buffer.getDouble(0)
}

2.9.3 用户自定义平均值函数 UDAF_AVG

import java.text.DecimalFormat

import org.apache.spark.sql.{Row, SparkSession}
import org.apache.spark.sql.expressions.{MutableAggregationBuffer, UserDefinedAggregateFunction}
import org.apache.spark.sql.types.{DataType, DoubleType, StringType, StructField, StructType}

/**
 * @Author jaffe
 * @Date 2020/05/14  23:35
 */
object UDAF_AVG {
  def main(args: Array[String]): Unit = {
    val spark = SparkSession.builder()
      .appName("UDAF_AVG")
      .master("local[2]")
      .getOrCreate()

    import spark.implicits._

    spark.udf.register("my_avg", new MyAVG)

    val df = spark.read.json("E:\\bigdata\\text\\input\\spark\\sql\\person.json")
    df.createOrReplaceTempView("p")
    spark.sql("select my_avg(age) from p").show()

    spark.close()
  }
}

class MyAVG extends UserDefinedAggregateFunction {
  //输入的数据类型 StructType 可以封装多个  Double
  override def inputSchema: StructType = StructType(StructField("ele", DoubleType) :: Nil)

  //缓冲区的数据类型 Double
  override def bufferSchema: StructType = StructType(StructField("sum", DoubleType) :: StructField("num", DoubleType) :: Nil)

  //最终聚合的返回值结果类型 Double
  override def dataType: DataType = StringType

  //确定性,相同的输入是否返回相同的输出  相同的输入有相同的输出
  override def deterministic: Boolean = true

  //初始化:对缓冲区初始化
  override def initialize(buffer: MutableAggregationBuffer): Unit = {
    buffer(0) = 0D
    buffer(1) = 0D
  }

  //分区内聚合  参数1:缓冲区 参数2:传过来的每行数据  行的列数由传给聚合函数的参数来定
  override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
    if (!input.isNullAt(0)) {
      buffer(0) = buffer.getDouble(0) + input.getDouble(0)
      buffer(1) = buffer.getDouble(1) + 1
    }

  }

  //分区间聚合 把buffer2中的数据,聚合到buffer1中
  override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
    //分区间的聚合一定是每个分区内聚合好的
    buffer1(0) = buffer1.getDouble(0) + buffer2.getDouble(0)
    buffer1(1) = buffer1.getDouble(1) + buffer2.getDouble(1)

  }

  override def evaluate(buffer: Row): Any =
    new DecimalFormat(".00").format(buffer.getDouble(0) / buffer.getDouble(1))
}

3.SparkSQL 数据源

3.1 通用加载和保存函数

默认数据源是parquet, 我们也可以通过使用:spark.sql.sources.default这个属性来设置默认的数据源.

val usersDF = spark.read.load("examples/src/main/resources/users.parquet")
usersDF.select("name", "favorite_color").write.save("namesAndFavColors.parquet")

说明:
1.spark.read.load 是加载数据的通用方法.
2.df.write.save 是保存数据的通用方法.

3.2 手动指定选项

手动给数据源指定一些额外的选项. 数据源应该用全名称来指定, 但是对一些内置的数据源也可以使用短名称:json, parquet, jdbc, orc, libsvm, csv, text

val peopleDF = spark.read.format("json").load("examples/src/main/resources/people.json")
peopleDF.select("name", "age").write.format("parquet").save("namesAndAges.parquet")

3.3 在文件上直接运行 SQL

我们前面都是使用read API 先把文件加载到 DataFrame, 然后再查询. 其实, 我们也可以直接在文件上进行查询

scala> spark.sql(“select * from json.examples/src/main/resources/people.json”)

说明:
json表示文件的格式. 后面的文件具体路径需要用反引号括起来.

3.4 文件保存选项(SaveMode)

保存操作可以使用 SaveMode, 用来指明如何处理数据. 使用mode()方法来设置.
这些 SaveMode 都是没有加锁的, 也不是原子操作. 如果你执行的是 Overwrite 操作, 在写入新的数据之前会先删除旧的数据.
在这里插入图片描述

3.5 使用JDBC进行读写

Spark SQL 可以通过 JDBC 从关系型数据库中读取数据的方式创建 DataFrame,通过对DataFrame一系列的计算后,还可以将数据再写回关系型数据库中
注意: 如果想在spark-shell操作 jdbc, 需要把相关的 jdbc 驱动 copy 到 jars 目录下
导入依赖:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.27</version>
</dependency>

3.5.1 从 jdbc 读数据

import org.apache.spark.sql.SparkSession

/**
 * @Author jaffe
 * @Date 2020/05/14  15:29
 */
object JDBCRead {

  val url = "jdbc:mysql://hadoop103:3306/gmall"
  val user = "root"
  val pwd = "123456"

  def main(args: Array[String]): Unit = {

    val spark = SparkSession
      .builder()
      .master("local[2]")
      .appName("JDBCRead")
      .getOrCreate()

    import spark.implicits._

    //通用的读法
    val df = spark.read.format("jdbc")
      .option("url", url)
      .option("user", user)
      .option("password", pwd)
      .option("dbtable", "user")
      .load()

    df.show()
    spark.close()

  }
}

3.5.2 向 jdbc 写入数据

import org.apache.spark.sql.SparkSession

/**
 * @Author jaffe
 * @Date 2020/05/15  00:48
 */
object JDBCWrite {

  val url = "jdbc:mysql://hadoop103:3306/gmall"
  val user = "root"
  val pwd = "123456"

  def main(args: Array[String]): Unit = {
    val spark = SparkSession
      .builder()
      .master("local[2]")
      .appName("JDBCRead")
      .getOrCreate()

    import spark.implicits._

    val df = spark.read.json("E:\\bigdata\\text\\input\\spark\\sql\\person.json")

    df.write.format("jdbc")
      .options(Map(
        "url" -> url,
        "user" -> user,
        "password" -> pwd,
        "dbtable" -> "user1128"
      ))
      .mode("append")
      .save()

    spark.close()
    
  }
}

3.6 Hive 数据库

若要把 Spark SQL 连接到一个部署好的 Hive 上,你必须把 hive-site.xml 复制到 Spark的配置文件目录中($SPARK_HOME/conf)。即使没有部署好 Hive,Spark SQL 也可以运行。

注意:如果你没有部署好Hive,Spark SQL 会在当前的工作目录中创建出自己的 Hive 元数据仓库,叫作 metastore_db。此外,如果你尝试使用 HiveQL 中的 CREATE TABLE (并非 CREATE EXTERNAL TABLE)语句来创建表,这些表会被放在你默认的文件系统中的 /user/hive/warehouse 目录中(如果你的 classpath 中有配好的 hdfs-site.xml,默认的文件系统就是 HDFS,否则就是本地文件系统)

3.6.1 使用内嵌的 Hive

如果使用 Spark 内嵌的 Hive, 则什么都不用做, 直接使用即可.
Hive 的元数据存储在 derby 中, 仓库地址:$SPARK_HOME/spark-warehouse

scala> spark.sql("show tables").show
+--------+---------+-----------+
|database|tableName|isTemporary|
+--------+---------+-----------+
+--------+---------+-----------+
scala> spark.sql("create table aa(id int)")
19/02/09 18:36:10 WARN HiveMetaStore: Location: file:/opt/module/spark-local/spark-warehouse/aa specified for non-external table:aa
res2: org.apache.spark.sql.DataFrame = []
scala> spark.sql("show tables").show
+--------+---------+-----------+
|database|tableName|isTemporary|
+--------+---------+-----------+
| default|       aa|      false|
+--------+---------+-----------+

向表中加载本地数据数据

scala> spark.sql("load data local inpath './ids.txt' into table aa")
res8: org.apache.spark.sql.DataFrame = []
scala> spark.sql("select * from aa").show
+---+
| id|
+---+
|100|
|101|
|102|
|103|
|104|
|105|
|106|
+---+

3.6.2 使用外置的 Hive

1.准备工作

  • Spark 要接管 Hive 需要把 hive-site.xml copy 到conf/目录下.
  • 把 Mysql 的驱动 copy 到 jars/目录下.
  • 如果访问不到hdfs, 则需要把core-site.xml和hdfs-site.xml 拷贝到conf/目录下.

2.启动 spark-shell

scala> spark.sql("show tables").show
+--------+---------+-----------+
|database|tableName|isTemporary|
+--------+---------+-----------+
| default|      emp|      false|
+--------+---------+-----------+
scala> spark.sql("select * from emp").show
19/02/09 19:40:28 WARN LazyStruct: Extra bytes detected at the end of the row! Ignoring similar problems.
+-----+-------+---------+----+----------+------+------+------+
|empno|  ename|      job| mgr|  hiredate|   sal|  comm|deptno|
+-----+-------+---------+----+----------+------+------+------+
| 7369|  SMITH|    CLERK|7902|1980-12-17| 800.0|  null|    20|
| 7499|  ALLEN| SALESMAN|7698| 1981-2-20|1600.0| 300.0|    30|
| 7521|   WARD| SALESMAN|7698| 1981-2-22|1250.0| 500.0|    30|
| 7566|  JONES|  MANAGER|7839|  1981-4-2|2975.0|  null|    20|
| 7654| MARTIN| SALESMAN|7698| 1981-9-28|1250.0|1400.0|    30|
| 7698|  BLAKE|  MANAGER|7839|  1981-5-1|2850.0|  null|    30|
| 7782|  CLARK|  MANAGER|7839|  1981-6-9|2450.0|  null|    10|
| 7788|  SCOTT|  ANALYST|7566| 1987-4-19|3000.0|  null|    20|
| 7839|   KING|PRESIDENT|null|1981-11-17|5000.0|  null|    10|
| 7844| TURNER| SALESMAN|7698|  1981-9-8|1500.0|   0.0|    30|
| 7876|  ADAMS|    CLERK|7788| 1987-5-23|1100.0|  null|    20|
| 7900|  JAMES|    CLERK|7698| 1981-12-3| 950.0|  null|    30|
| 7902|   FORD|  ANALYST|7566| 1981-12-3|3000.0|  null|    20|
| 7934| MILLER|    CLERK|7782| 1982-1-23|1300.0|  null|    10|
| 7944|zhiling|    CLERK|7782| 1982-1-23|1300.0|  null|    50|
+-----+-------+---------+----+----------+------+------+------+

3.使用spark-sql cli
在spark-shell执行 hive 方面的查询比较麻烦.spark.sql("").show
Spark 专门给我们提供了书写 HiveQL 的工具: spark-sql cli

4.使用hiveserver2 + beeline
spark-sql 得到的结果不够友好, 所以可以使用hiveserver2 + beeline

  1. 启动 thrift服务器
    sbin/start-thriftserver.sh
    –master yarn \

  2. 启动beeline客户端
    bin/beeline
    然后输入
    !connect jdbc:hive2://hadoop103:10000
    然后按照提示输入用户名和密码

3.6.3 在代码中访问 Hive

步骤1: 拷贝 hive-site.xml 到 resources 目录下
步骤2: 添加依赖

<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-hive_2.11</artifactId>
    <version>2.1.1</version>
</dependency>

步骤3: 代码

1.查数据

import org.apache.spark.sql.SparkSession

/**
 * @Author jaffe
 * @Date 2020/05/15  11:04
 */
object HiveDemo {
  System.setProperty("HADOOP_USER_NAME","jaffe")
  def main(args: Array[String]): Unit = {
    val spark = SparkSession
      .builder()
      .master("local[2]")
      .appName("HiveDemo")
      .enableHiveSupport()
      .getOrCreate()

    import spark.implicits._
    spark.sql("use gmall").show
    spark.sql("show tables").show
    //spark.sql("show tables").show
   // spark.sql("select count(*) from ods_activity_info").show

    spark.close()
  }
}

2.写数据

import org.apache.spark.sql.SparkSession

object HiveWrite {
  def main(args: Array[String]): Unit = {
    //设置权限,一般写的时候涉及到权限
    System.setProperty("HADOOP_USER_NAME", "jaffe")
    val spark: SparkSession = SparkSession
      .builder()
      .master("local[*]")
      .appName("HiveWrite")
      .enableHiveSupport()
      // 配置仓库的地址,没有配置的话默认是在本地仓库
      .config("spark.sql.warehouse.dir", "hdfs://hadoop102:9000/user/hive/warehouse")
      .getOrCreate()
    spark.sql("use spark1128")
    // 1. 先有df
    import spark.implicits._
    val df = spark.read.json("E:\\bigdata\\text\\input\\spark\\sql\\person.json")
    //val df = List(("a", 11L), ("b", 22L)).toDF
    df.printSchema()
    // 2. 写法1: 使用saveAsTable
    //df.write.saveAsTable("user_1")
    // df.write.mode("append").saveAsTable("user_1")

    // 3. 写法2: 使用 insertInto
    df.write.insertInto("user_1") // 大致等价于: df.write.mode("append").saveAsTable("user_1")
    // df.write.insertInto("user_1")  // 大致等价于: df.write.mode("append").saveAsTable("user_1")

    // 4. 写法3: 使用 hive的insert 语句

    //        spark.sql("insert into table user_1 values('zs', 100)").show
    //
    spark.sql("select * from user_1").show

    // 创建数据库
    //spark.sql("create database test1129").show
    // spark.sql("show databases").show
    spark.close()
  }
}

/*
读的时候不需要权限, 写的时候一般才需要权限.

saveAsTable
  在保存的时候, 看列名, 只要列名一致, 顺序不重要
insertInto
    不看列名, 只看顺序(类型):
 */

4.SparkSql 项目实战

4.1 准备数据

在 Hive 中创建表, 并导入数据.
一共有 3 张表: 1 张用户行为表, 1 张城市表, 1 张产品表

CREATE TABLE `user_visit_action`(
  `date` string,
  `user_id` bigint,
  `session_id` string,
  `page_id` bigint,
  `action_time` string,
  `search_keyword` string,
  `click_category_id` bigint,
  `click_product_id` bigint,
  `order_category_ids` string,
  `order_product_ids` string,
  `pay_category_ids` string,
  `pay_product_ids` string,
  `city_id` bigint)
row format delimited fields terminated by '\t';
load data local inpath '/opt/module/datas/user_visit_action.txt' into table test1129.user_visit_action;

CREATE TABLE `product_info`(
  `product_id` bigint,
  `product_name` string,
  `extend_info` string)
row format delimited fields terminated by '\t';
load data local inpath '/opt/module/datas/product_info.txt' into table test1129.product_info;

CREATE TABLE `city_info`(
  `city_id` bigint,
  `city_name` string,
  `area` string)
row format delimited fields terminated by '\t';
load data local inpath '/opt/module/datas/city_info.txt' into table test1129.city_info;

4.2 需求:各区域热门商品 Top3

热门商品是从点击量的维度来看的
计算各个区域前三大热门商品,并备注上每个商品在主要城市中的分布比例,超过两个城市用其他显示

例如:

地区商品名称点击次数城市备注
华北商品A100000北京21.2%,天津13.2%,其他65.6%
华北商品P80200北京63.0%,太原10%,其他27.0%
华北商品M40000北京63.0%,太原10%,其他27.0%
东北商品J92000大连28%,辽宁17.0%,其他 55.0%

4.3 思路分析

  1. 先把需要的字段, join, 查出来,并按照地区、商品名称分组,统计出每个商品在每个地区的总点击次数
  2. 开窗,每个地区内按照点击次数降序排列
  3. 只取前三名. 并把结果保存在数据库中
  4. 城市备注需要自定义 UDAF 函数

4.4 代码实现

import java.text.DecimalFormat

import org.apache.spark.sql.{Row, SaveMode, SparkSession}
import org.apache.spark.sql.expressions.{MutableAggregationBuffer, UserDefinedAggregateFunction}
import org.apache.spark.sql.types.{DataType, LongType, MapType, StringType, StructField, StructType}

/**
 * @Author jaffe
 * @Date 2020/05/15  14:59
 */

/*----
各区域热门商品 Top3
地区	商品名称		点击次数	城市备注
华北	商品A		100000	北京21.2%,天津13.2%,其他65.6%
华北	商品P		80200	北京63.0%,太原10%,其他27.0%
华北	商品M		40000	北京63.0%,太原10%,其他27.0%
东北	商品J		92000	大连28%,辽宁17.0%,其他 55.0%

----*/
object SqlApp {
  def main(args: Array[String]): Unit = {
    System.setProperty("HADOOP_USER_NAME", "jaffe")
    val spark: SparkSession = SparkSession
      .builder()
      .master("local[*]")
      .appName("SqlApp")
      .enableHiveSupport()
      .config("spark.sql.shuffle.partitions", 10)
      .getOrCreate()

    spark.sql("use spark1128")

    spark.udf.register("remark", new CityRemarkUDAF)
    spark.sql(
      """
        |SELECT
        |t2.area,
        |t2.product_name,
        |count,
        |remark
        |from
        |(SELECT
        |t1.area,
        |t1.product_name,
        |remark,
        |count,
        |rank() over(PARTITION by t1.area order by count desc) rk
        |from
        |(select
        |remark(ci.city_name) remark,
        |ci.area,
        |pi.product_name,
        |COUNT(*) count
        |from user_visit_action uva JOIN
        |city_info ci on uva.city_id = ci.city_id JOIN
        |product_info pi on uva.click_product_id = pi.product_id
        |group by ci.area,pi.product_name) t1
        |) t2
        |where rk <= 3
        |""".stripMargin
    ).coalesce(1) // 降低分区
      //      .write
      //      .mode(SaveMode.Overwrite)
      //      .saveAsTable("result")
      //spark.sql("select * from result").show
      .show(1000,false)//显示1000行,默认true是截断显示行内容,false是完整显示行内容

    spark.close()

  }
}

// 城市备注的聚合函数
class CityRemarkUDAF extends UserDefinedAggregateFunction {
  // 输入数据的类型    城市名   StringType
  override def inputSchema: StructType = StructType(StructField("city", StringType) :: Nil)

  // 北京->1000 天津->10  Map 缓冲区   MapType(key类型, value类型)
  // 最好再缓冲一个总数
  override def bufferSchema: StructType = StructType(StructField("map", MapType(StringType, LongType)) :: StructField("total", LongType) :: Nil)

  // 输出类型  StringType
  override def dataType: DataType = StringType

  // 确定性
  override def deterministic: Boolean = true

  // 初始化  对缓冲区初始化
  override def initialize(buffer: MutableAggregationBuffer): Unit = {
    buffer(0) = Map[String, Long]()
    buffer(1) = 0L
  }

  // 分区内聚合
  override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {

    input match {
      // 传入了一个城市
      case Row(cityName: String) =>
        // 北京
        // 1. 先更新总数
        buffer(1) = buffer.getLong(1) + 1L
        // 2. 再去更新具体城市的数量
        val map = buffer.getMap[String, Long](0)
        buffer(0) = map + (cityName -> (map.getOrElse(cityName, 0L) + 1L))
      // 传入了一个null
      case _ =>
    }
  }

  // 分区间聚合
  override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
    // 把数据合并, 再放入到buffer1中
    val map1 = buffer1.getMap[String, Long](0)
    val total1 = buffer1.getLong(1)

    val map2 = buffer2.getMap[String, Long](0)
    val total2 = buffer2.getLong(1)

    buffer1(1) = total1 + total2

    buffer1(0) = map1.foldLeft(map2) {
      case (map, (city, count)) =>
        map + (city -> (map.getOrElse(city, 0L) + count))

    }
  }

  // 返回最终的值
  override def evaluate(buffer: Row): Any = {
    val cityCount = buffer.getMap[String, Long](0)
    val total = buffer.getLong(1)

    val cityCountTop2 = cityCount.toList.sortBy(-_._2).take(2)

    val cityRemarkTop2: List[CityRemark] = cityCountTop2.map {
      case (city, count) => CityRemark(city, count.toDouble / total)
    }

    val cityRemarks = cityRemarkTop2 :+ CityRemark("其它", cityRemarkTop2.foldLeft(1D)(_ - _.rate))

    cityRemarks.mkString(", ")


  }
}

case class CityRemark(city: String, rate: Double) {
  val f = new DecimalFormat("#.##%")

  override def toString: String = s"$city:${f.format(math.abs(rate))}"
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值