Spark大数据开发与应用案例(视频教学版)(十五)--第十章上

本书及作者信息

作者: 余辉
微信公众号:辉哥大数据
备注:本书配套示例源码、PPT课件、教学视频与作者答疑服务,每周更新一部分,敬请期待…
红包奖励:找出本书问题,评论区并指正,且留下联系方式

本书其他章节

  1. Spark大数据开发与应用案例(视频教学版)(一)–文前
  2. Spark大数据开发与应用案例(视频教学版)(二)–第一章上
  3. Spark大数据开发与应用案例(视频教学版)(三)–第一章下
  4. Spark大数据开发与应用案例(视频教学版)(四)–第二章上
  5. Spark大数据开发与应用案例(视频教学版)(五)–第二章下
  6. Spark大数据开发与应用案例(视频教学版)(六)–第三章上
  7. Spark大数据开发与应用案例(视频教学版)(七)–第三章下
  8. Spark大数据开发与应用案例(视频教学版)(八)–第四章上
  9. Spark大数据开发与应用案例(视频教学版)(九)–第四章下
  10. Spark大数据开发与应用案例(视频教学版)(十)–第五章
  11. Spark大数据开发与应用案例(视频教学版)(十一)–第六章
  12. Spark大数据开发与应用案例(视频教学版)(十二)–第七章
  13. Spark大数据开发与应用案例(视频教学版)(十三)–第八章
  14. Spark大数据开发与应用案例(视频教学版)(十四)–第九章

第10章 Spark SQL抽象编程详解

       本章将引领你深入探索DataFrame和DataSet 的奥秘,从创建到输出,再到各类运算,全面展现其数据处理能力。同时,你还将学习如何在RDD与SQL代码间自由切换,掌握RDD、DataSet、DataFrame间的互转技巧。本章是你掌握DataFrame编程的必备指南。
       本章主要知识点:

  • DataFrame创建
  • DataFrame运算
  • DataFrame输出
  • RDD代码和SQL代码混合编程
  • RDD/DS/DF互转

10.1 DataFrame创建

       在Spark SQL中SparkSession是创建DataFrames和执行SQL的入口。创建DataFrames有三种方式:

  • 从一个已存在的RDD进行转换。
  • 从JSON/Parquet/CSV/ORC等结构化文件源创建。
  • 从Hive/JDBC各种外部结构化数据源(服务)创建。
           创建DataFrame,需要创建“RDD + 元信息schema定义”。RDD来自于数据,Schema则可以由开发人员定义,或者由框架从数据中推断。下文将通过具体案例带领读者进入实战练习环节。

10.1.1 使用RDD创建DataFrame

  1. 将RDD关联case class创建DataFrame。示例代码如下:
    代码10-1 DataFrameDemo1.scala
object DataFrameDemo1 {

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

    val conf = new SparkConf()
      .setAppName(this.getClass.getSimpleName)
      .setMaster("local[*]")
    //1.SparkSession,是对SparkContext的增强
    val session: SparkSession = SparkSession.builder()
      .config(conf)
      .getOrCreate()

    //2.创建DataFrame
    //2.1先创建RDD
    val lines: RDD[String] = session.sparkContext.textFile("data/user.txt")
    //2.2对数据进行整理并关联Schema
    val tfBoy: RDD[Boy] = lines.map(line => {
      val fields = line.split(",")
      val name = fields(0)
      val age = fields(1).toInt
      val fv = fields(2).toDouble
      Boy(name, age, fv) //字段名称,字段的类型
    })

    //2.3将RDD关联schema,将RDD转成DataFrame
    //导入隐式转换
    import session.implicits._
    val df: DataFrame = tfBoy.toDF
    //打印DataFrame的Schema信息
    df.printSchema()
    //3.将DataFrame注册成视图(虚拟的表)
    df.createTempView("v_users")
    //4.写sql(Transformation)
    val df2: DataFrame = session.sql("select * from v_users order by fv desc, age asc")
    //5.触发Action
    df2.show()
    //6.释放资源
    session.stop()
  }
}

case class Boy(name: String, age: Int, fv: Double)

执行结果如图10-1所示。
在这里插入图片描述
2. 将RDD关联scala class创建DataFrame,示例代码如下:
代码10-2 DataFrameDemo2.scala

package chapter10

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

import scala.beans.BeanProperty

object DataFrameDemo2 {

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

    //1.创建SparkSession
    val spark = SparkSession.builder()
      .appName("DataFrameDemo2")
      .master("local[*]")
      .getOrCreate()

    //2.创建RDD
    val lines: RDD[String] = spark.sparkContext.textFile("BookData/input/10boy.txt")
    //2将数据封装到普通的class中
    val boyRDD: RDD[Boy2] = lines.map(line => {
      val fields = line.split(",")
      val name = fields(0)
      val age = fields(1).toInt
      val fv = fields(2).toDouble
      new Boy2(name, age, fv) //字段名称,字段的类型
    })
    //3.将RDD和Schema进行关联
    val df = spark.createDataFrame(boyRDD, classOf[Boy2])
    //df.printSchema()
    //4.使用DSL风格的API
    import spark.implicits._
    df.show()
    spark.stop()
  }
}

//参数前面必须有var或val
//必须添加给字段添加对应的getter方法,在scala中,可以@BeanProperty注解
class Boy2(
            @BeanProperty
            val name: String,
            @BeanProperty
            val age: Int,
            @BeanProperty
            val fv: Double) {

}

执行结果如图10-2所示。注意,普通的Scala class必须在成员变量加上@BeanProperty属性,因为Spark SQL需要通过反射调用getter获取schema信息。
在这里插入图片描述
3. 将RDD关联java class创建DataFrame,示例代码如下:
代码10-3 JBoy的javabean的JBoy .scala

package chapter10;

public class JBoy {

    private String name;
    private Integer age;
    private Double fv;

    public String getName() {
        return name;
    }

    public Integer getAge() {
        return age;
    }

    public Double getFv() {
        return fv;
    }

    public JBoy(String name, Integer age, Double fv) {
        this.name = name;
        this.age = age;
        this.fv = fv;
    }
}

代码10-4 主函数DataFrameDemo3.scala

package chapter10

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

object DataFrameDemo3 {

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

    val spark: SparkSession = SparkSession.builder()
      .appName(this.getClass.getSimpleName)
      .master("local[*]")
      .getOrCreate()

    val lines = spark.sparkContext.textFile("BookData/input/10boy.txt")
    //将RDD关联的数据封装到Java的class中,但是依然是RDD
    val jboyRDD: RDD[JBoy] = lines.map(line => {
      val fields = line.split(",")
      new JBoy(fields(0), fields(1).toInt, fields(2).toDouble)
    })
    //强制将关联了schema信息的RDD转成DataFrame
    val df: DataFrame = spark.createDataFrame(jboyRDD, classOf[JBoy])
    df.printSchema()

    //注册视图
    df.createTempView("v_boy")
    //写sql
    val df2: DataFrame = spark.sql("select name, age, fv from v_boy order by fv desc, age asc")
    df2.show()
    spark.stop()
  }
}

执行结果如图10-3所示。
在这里插入图片描述
4. 将RDD关联Schema创建DataFrame,示例代码如下:
代码10-4 DataFrameDemo4.scala

package chapter10

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

object DataFrameDemo4 {

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

    val spark: SparkSession = SparkSession.builder()
      .appName(this.getClass.getSimpleName)
      .master("local[*]")
      .getOrCreate()

    val lines = spark.sparkContext.textFile("BookData/input/10boy.txt")
    //将RDD关联了Schema,但是依然是RDD
    val rowRDD: RDD[Row] = lines.map(line => {
      val fields = line.split(",")
      Row(fields(0), fields(1).toInt, fields(2).toDouble)
    })

    val schema = StructType.apply(
      List(
        StructField("name", StringType),
        StructField("age", IntegerType),
        StructField("fv", DoubleType)
      )
    )
    val df: DataFrame = spark.createDataFrame(rowRDD, schema)
    //打印schema信息
    df.printSchema()
    //注册视图
    df.createTempView("v_boy")
    //写sql
    val df2: DataFrame = spark.sql("select name, age, fv from v_boy order by fv desc, age asc")
    df2.show()
    spark.stop()
  }
}

执行结果如图10-4所示。
在这里插入图片描述

10.1.2 从结构化文件创建DataFrame

  1. 从csv文件(不带header)中创建
    文件名称为10stu.csv,文件内容如下:
1,张飞,21,北京,80.0
2,关羽,23,北京,82.0
3,赵云,20,上海,88.6
4,刘备,26,上海,83.0
5,曹操,30,深圳,90.0

从csv文件(不带header)进行创建,示例代码如下:
代码10-5 DataFrameDemo5.scala

package chapter10
import org.apache.spark.sql.SparkSession
object DataFrameDemo5 {
  def main(args: Array[String]): Unit = {
    val spark: SparkSession = SparkSession.builder()
      .appName(this.getClass.getSimpleName)
      .master("local[*]")
      .getOrCreate()
    val df = spark.read.csv("BookData/input/10stu.csv")
    df.printSchema()
    df.show()
  }
}

执行结果如图10-5所示。
在这里插入图片描述
       可以上图可以看出,框架把读取进来的csv数据自动生成的schema,其字段名为_c0,_c1,_c2,_c3,_c4。字段类型全为String。但这种数据格式不一定符合我们的需求。下面我们使用自定义Schema创建DataFrame。
       2. 从csv文件(不带header)自定义Schema进行创建
创建DataFrame时,传入自定义的schema。schema在API中用StructType这个类来描述,字段用StructField来描述。示例代码如下:
代码10-6 DataFrameDemo6.scala

package chapter10
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.types.{DataTypes, StructType}

object DataFrameDemo6 {
  def main(args: Array[String]): Unit = {
    val spark: SparkSession = SparkSession.builder()
      .appName(this.getClass.getSimpleName)
      .master("local[*]")
      .getOrCreate()
    val schema = new StructType()
      .add("id", DataTypes.IntegerType)
      .add("name", DataTypes.StringType)
      .add("age", DataTypes.IntegerType)
      .add("city", DataTypes.StringType)
      .add("score", DataTypes.DoubleType)
    val df = spark.read.schema(schema).csv("BookData/input/10stu.csv")
    df.printSchema()
    df.show()
  }
}

执行结果如图10-6所示。
在这里插入图片描述
3. 从csv文件(带header)进行创建
文件名称10stu2.csv,文件内容如下:

id,name,age,city,score
1,张飞,21,北京,80.0
2,关羽,23,北京,82.0
3,赵云,20,上海,88.6
4,刘备,26,上海,83.0
5,曹操,30,深圳,90.0

示例代码如下:
代码10-7 DataFrameDemo7.scala

package chapter10
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.types.{DataTypes, StructType}
object DataFrameDemo7 {
  def main(args: Array[String]): Unit = {
    val spark: SparkSession = SparkSession.builder()
      .appName(this.getClass.getSimpleName)
      .master("local[*]")
      .getOrCreate()
    val df = spark.read
      .option("header",true) //读取表头信息
      .csv("BookData/input/10stu2.csv")
    df.printSchema()
    df.show()
  }
}

执行结果如图10-7所示。
在这里插入图片描述
       注意,此文件的第一行是字段描述信息,需要特别处理,否则会被当做RDD中的一行数据。这个处理的关键点是设置一个header=true的参数,代码示例如下:

val df = spark.read
  .option("header",true) //读取表头信息
  .csv("data_ware/demodata/stu.csv")
df.printSchema()
df.show()

注意,虽然字段名正确指定,但是字段类型还是无法确定,默认情况下全视作String对待,当然,可以开启一个参数inferSchema=true来让框架对csv中的数据字段进行合理的类型推断。

val df = spark.read
  .option("header",true)
  .option("inferSchema",true)  //推断字段类型
  .csv("data_ware/demodata/stu.csv")
df.printSchema()
df.show()

注意,如果推断的结果不如人意,当然可以指定自定义schema。让框架自动推断schema效率低,不建议使用!
4. 从JSON文件进行创建
文件名称10student.json,文件内容如下:

{"name":"A","lesson":"Math","score":100}
{"name":"B","lesson":"Math","score":100}
{"name":"C","lesson":"Math","score":99}
{"name":"D","lesson":"Math","score":98}
{"name":"A","lesson":"E","score":100}
{"name":"B","lesson":"E","score":99}
{"name":"C","lesson":"E","score":99}
{"name":"D","lesson":"E","score":98}

示例代码10-8如下:
代码10-8 DataFrameDemo8.scala

package chapter10
import org.apache.spark.sql.SparkSession
object DataFrameDemo8 {

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

    val spark: SparkSession = SparkSession.builder()
      .appName(this.getClass.getSimpleName)
      .master("local[*]")
      .getOrCreate()

    val df = spark.read.json("BookData/input/10student.json")
    df.printSchema()
    df.show()
  }
}

执行结果如图10-8所示。
在这里插入图片描述
5. 从Parquet文件进行创建
Parquet文件是一种列式存储文件格式,文件自带schema描述信息,任意拿一个DataFrame,调用write.parquet()方法即可将df保存为一个parquet文件。代码示例:

val df = spark.read.parquet("data/parquet/")
  1. 从orc文件进行创建
    ORC文件是一种列式存储文件格式,文件自带schema描述信息,任意拿一个DataFrame,调用.write.format(“orc”).save(“/path”)方法即可将df保存为一个ORC文件。代码示例:
val df = spark.read.orc("data/orcfiles/")

10.1.3 外部存储服务创建DF

       从JDBC连接数据库服务器进行创建。
1. 实验数据准备
       在一个MySQL服务器中,创建一个数据库spark,在库中再创建一个表student,如图10-9所示。
在这里插入图片描述
要使用JDBC连接读取数据库的数据,需要引入JDBC的驱动jar包依赖:

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

代码示例:

val props = new Properties()
	props.setProperty("user","root")
	props.setProperty("password","yuhui888")
	val df = spark.read.jdbc("jdbc:mysql://yuhui01:3306/spark","student",props)
	df.show()

执行结果如下:

+---+------+---+------+-----+
| id|name  |age|city  |score|
+---+------+---+------+-----+
|  1|   张飞| 21|   北京| 80.0|
|  2|   关羽| 23|   北京| 82.0|
|  3|   赵云| 20|   上海| 88.6|
|  4|   刘备| 26|   上海| 83.0|
|  5|   曹操| 30|   深圳| 90.0|
+---+------+---+------+-----+

Spark SQL添加了spark-hive的依赖,并在sparkSession构造时开启enableHiveSupport,之后就整合了Hive的功能(通俗地说,就是Spark SQL具备了Hive的功能),spark-hive的依赖和依赖中的组件如图10-10所示。
在这里插入图片描述
既然具备了Hive的功能,那么就可以执行Hive中所有能执行的动作,包括建表、show表、建库、show库、alter表,等等。只不过,此时看见的表是Spark中集成的Hive的本地元数据库中的表。
如果想让Spark中集成的Hive看见你外部集群中的Hive表,只要修改配置:把Spark端的Hive的元数据服务地址指向外部集群中Hive的元数据服务地址。有两种指定办法:
在Spark端加入hive-site.xml,里面配置目标元数据库MySQL的连接信息,这会使得Spark中集成的Hive直接访问MySQL元数据库。hive-site.xml配置如下:

<configuration>
    <!-- 使用JDBC连接到MySQL数据库,自动创建数据库(如果不存在) -->
    <property>
        <name>javax.jdo.option.ConnectionURL</name>        <value>jdbc:mysql://<mysql-host>:<mysql-port>/<hive-metastore-db>?createDatabaseIfNotExist=true</value>
    </property>
    <!-- 配置MySQL数据库JDBC连接驱动 -->
    <property>
        <name>javax.jdo.option.ConnectionDriverName</name>
        <value>com.mysql.jdbc.Driver</value>
    </property>
    <!-- 配置连接数据库的用户名 -->
    <property>
        <name>javax.jdo.option.ConnectionUserName</name>
        <value><mysql-username></value>
    </property>
    <!-- 配置连接数据库的密码 -->
    <property>
        <name>javax.jdo.option.ConnectionPassword</name>
        <value><mysql-password></value>
    </property>
    <!-- 注释:以下可能包含其他Hive相关的配置信息,具体配置根据需求添加 -->
</configuration>

在Spark端加入hive-site.xml,里面配置目标Hive的元数据服务器地址,这会使得Spark中集成的Hive通过外部独立的Hive元数据服务来访问元数据库。hive-site.xml配置如下:

<configuration>
    <!-- HIVE元数据配置 -->
    <property>
        <name>hive.metastore.uris</name>
        <value>thrift://<metastore-host>:<metastore-port></value>
    </property>
    <!-- 其他Hive配置 -->
</configuration>
  1. 从Hive创建DataFrame
    Spark SQL通过spark-hive整合包来集成Hive的功能。Spark SQL加载“外部独立Hive”的数据,本质上是不需要外部独立Hive参与的,因为外部独立Hive的表数据就在HDFS中,元数据信息在MySQL中不管数据还是元数据,Spark SQL都可以直接去获取!
    从Hive创建DataFrame的操作步骤如下:
    要在工程中添加spark-hive的依赖jar以及MySQL的JDBC驱动jar。
<!-- MySQL的依赖-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.30</version>
</dependency>

<!-- Spark整合Hive的依赖,即可以读取Hive的源数据库,使用Hive特点的SQL-->
<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-hive_2.13</artifactId>
    <version>3.5.3</version>
</dependency>
要在工程中添加hive-site.xml、core-site.xml和hdfs-site.xml配置文件。

在这里插入图片描述
在hive-site.xml中配置Hive元数据服务地址信息。

<configuration>
    <property>
        <name>hive.metastore.uris</name>
        <value>thrift://yuhui01:9083</value>
    </property>
</configuration>
创建sparksession时需要调用.enableHiveSupport()方法。
val spark = SparkSession
  .builder()
  .appName(this.getClass.getSimpleName)
  .master("local[*]")
  // 启用hive支持,需要调用enableHiveSupport,还需要添加一个依赖 spark-hive
  // 默认sparksql内置了自己的hive
  // 如果程序能从classpath中加载到hive-site配置文件,那么它访问的hive元数据库就不是本地内置的了,而是配置中所指定的元数据库了
  // 如果程序能从classpath中加载到core-site配置文件,那么它访问的文件系统也不再是本地文件系统了,而是配置中所指定的hdfs文件系统了
  .enableHiveSupport()
  .getOrCreate()
加载Hive中的表。
val df = spark.sql("select * from t1")

注意,如果你也用DataFrame注册了一个同名的视图,那么这个视图名会替换掉Hive的表。
3. 从HBase创建DataFrame
Spark SQL可以连接任意外部数据源(只要有对应的“连接器”即可)Spark SQL对HBase是有第三方连接器(华为)的,但是久不维护。建议用Hive作为连接器(Hive可以访问HBase,而Spark SQL可以集成Hive)在HBase中建表:

create 'bigdata_stu','f'

插入数据到HBase表bigdata_stu:

put 'bigdata_stu','001','f:name','zhangsan'
put 'bigdata_stu','001','f:name','张三'
put 'bigdata_stu','001','f:age','26'
put 'bigdata_stu','001','f:gender','m'
put 'bigdata_stu','001','f:salary','28000'
put 'bigdata_stu','002','f:name','lisi'
put 'bigdata_stu','002','f:age','22'
put 'bigdata_stu','002','f:gender','m'
put 'bigdata_stu','002','f:salary','26000'
put 'bigdata_stu','003','f:name','wangwu'
put 'bigdata_stu','003','f:age','21'
put 'bigdata_stu','003','f:gender','f'
put 'bigdata_stu','003','f:salary','24000'
put 'bigdata_stu','004','f:name','zhaoliu'
put 'bigdata_stu','004','f:age','22'
put 'bigdata_stu','004','f:gender','f'
put 'bigdata_stu','004','f:salary','25000'
创建Hive外部表映射HBase中的表:
CREATE EXTERNAL TABLE bigdata_stu
( 
  id        string ,
  name      string ,
  age       int    ,
  gender    string ,
  salary    double  
)  
STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler' 
WITH SERDEPROPERTIES ( 'hbase.columns.mapping'=':key,f:name,f:age,f:gender,f:salary') 
TBLPROPERTIES ( 'hbase.table.name'='default:bigdata_stu')
;

工程中放置hbase-site.xml配置文件:

<configuration>
    <!-- 配置HBase的根目录 -->
    <property>
        <name>hbase.rootdir</name>
        <value>hdfs://yuhui01:8020/hbase</value>
    </property>
    <!-- 配置HBase是否为分布式集群模式 -->
    <property>
        <name>hbase.cluster.distributed</name>
        <value>true</value>
    </property>
    <!-- 配置HBase使用的ZooKeeper集群地址 -->
    <property>
        <name>hbase.zookeeper.quorum</name>
        <value>yuhui01:2181,yuhui02:2181,yuhui03:2181</value>
    </property>
    <!-- 配置HBase是否强制检查文件流的能力 -->
    <property>
        <name>hbase.unsafe.stream.capability.enforce</name>
        <value>false</value>
    </property>
</configuration>

工程中添加hive-hbase-handler连接器依赖:

<dependency>
    <groupId>org.apache.hive</groupId>
    <artifactId>hive-hbase-handler</artifactId>
    <version>2.3.7</version>
    <exclusions>
        <exclusion>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-common</artifactId>
        </exclusion>
    </exclusions>
</dependency>

以读取Hive表的方式直接读取即可:

spark.sql("select * from bigdata_stu")

10.2 DataFrame运算

       本节将通过Spark SQL的两种风格对DataFrame进行运算,分别是SQL操作和DSL风格。

10.2.1 SQL风格操作

将DataFrame 注册为一个临时视图view,然后就可以针对view直接执行各种SQL。临时视图有两种:Session级别视图和Global级别视图。
Session级别视图在Session范围内有效,Session退出后,表就失效了。
Global全局视图则在application级别有效。
注意,使用全局表时需要全路径访问global_temp.people。

// application全局有效
df.createGlobalTempView("stu")
spark.sql(
  """
    |select * from global_temp.stu a order by a.score desc
  """.stripMargin)
    .show()

// session有效
df.createTempView("s")
spark.sql(
  """
    |select * from s order by score
  """.stripMargin)
  .show()

val spark2 = spark.newSession()

// 全局有效的view可以在session2中访问
spark2.sql("select id,name from global_temp.stu").show()

// session有效的view不能在session2中访问
spark2.sql("select id,name from s").show()

10.2.2 DSL风格API(TableApi)语法

在Apache Spark中,DSL(Domain-Specific Language)风格的API主要指的是Spark SQL和DataFrame API,它们提供了一种类似于SQL的表达式语言来操作数据。这些API使得数据处理更加直观和易于理解,尤其是对于熟悉SQL的数据科学家和分析师来说。

  1. 数据准备
val df = spark.read
  .option("header", true)
  .option("inferSchema", true)
  .csv("data_ware/demodata/stu.csv")
  1. 基本select及表达式
/**
  * 逐行运算
  */
// 使用字符串表达"列"
df.select("id","name").show()

// 如果要用字符串形式表达sql表达式,应该使用selectExpr方法
df.selectExpr("id+1","upper(name)").show

// select方法中使用字符串sql表达式,会被视作一个列名从而出错
// df.select("id+1","upper(name)").show()

import spark.implicits._
// 使用$符号创建Column对象来表达"列"
df.select($"id",$"name").show()

// 使用单边单引号创建Column对象来表达"列"
df.select('id,'name).show()

// 使用col函数来创建Column对象来表达"列"
import org.apache.spark.sql.functions._
df.select(col("id"),col("name")).show()

// 使用Dataframe的apply方法创建Column对象来表达列
df.select(df("id"),df("name")).show()

// 对Column对象直接调用Column的方法,或调用能生成Column对象的functions来实现sql中的运算表达式
df.select('id.plus(2).leq("4").as("id2"),upper('name)).show()
df.select('id+2 <= 4 as "id2",upper('name)).show()
  1. 字段重命名
/**
  * 字段重命名
  */
// 对column对象调用as方法
df.select('id as "id2",$"name".as("n2"),col("age") as "age2").show()

// 在selectExpr中直接写sql的重命名语法
df.selectExpr("cast(id as string) as id2","name","city").show()

// 对dataframe调用withColumnRenamed方法对指定字段重命名
df.select("id","name","age").withColumnRenamed("id","id2").show()

// 对dataframe调用toDF对整个字段名全部重设
df.toDF("id2","name","age","city2","score").show()
  1. 条件过滤
/**
  * 逐行过滤
  */
df.where("id>4 and score>95")
df.where('id > 4 and 'score > 95).select("id","name","age").show()
  1. 分组聚合
/**
  * 分组聚合
  */
df.groupBy("city").count().show()
df.groupBy("city").min("score").show()
df.groupBy("city").max("score").show()
df.groupBy("city").sum("score").show()
df.groupBy("city").avg("score").show()​
df.groupBy("city").agg(("score","max"),("score","sum")).show()
df.groupBy("city").agg("score"->"max","score"->"sum").show()
  1. 子查询
/**
  * 子查询相当于:
  * select
  * *
  * from 
  * (
  *   select
  *   city,sum(score) as score
  *   from stu
  *   group by city
  * ) o
  * where score>165
  */
df.groupBy("city")
  .agg(sum("score") as "score")
  .where("score > 165")
  .select("city", "score")
  .show()
  1. Join关联查询
/**
   * 总结:
   * join方式:  joinType: String
   * join条件:
   * 可以直接传join列名:usingColumn/usingColumns : Seq(String) 注意:右表的join列数据不会出现结果中
   * 可以传join自定义表达式: Column.+(1) === Column  df1("id")+1 === df2("id")
   */

  // 笛卡儿积
  df1.crossJoin(df2).show()

  // 给join传入一个连接条件; 这种方式,要求,你的join条件字段在两个表中都存在且同名
  df1.join(df2,"id").show()

  // 传入多个join条件列,要求两表中这多个条件列都存在且同名
  df1.join(df2,Seq("id","sex")).show()

  // 传入一个自定义的连接条件表达式
  df1.join(df2,df1("id") + 1 === df2("id")).show()

  // 还可以传入join方式类型: inner(默认), left ,right, full ,left_semi, left_anti
  df1.join(df2,df1("id")+1 === df2("id"),"left").show()
  df1.join(df2,Seq("id"), "right").show()
  1. Union操作
    在Spark SQL中,UNION 和 UNION ALL 都要求合并的查询结果具有相同的字段个数、字段名称和字段类型,其中 UNION ALL 保留所有行而 UNION 去除重复行。
df1.union(df2).show()
  1. 窗口分析函数调用
    文件名称10stu2.csv,文件内容如下:
id,name,age,sex,city,score
1,张飞,21,M,北京,80
2,关羽,23,M,北京,82
7,周瑜,24,M,北京,85
3,赵云,20,F,上海,88
4,刘备,26,M,上海,83
8,孙权,26,M,上海,78
5,曹操,30,F,深圳,90.8
6,孔明,35,F,深圳,77.8
9,吕布,28,M,深圳,98

求每个城市中成绩最高的两个人的信息,使用SQL编写:

select 
id,name,age,sex,city,score
from 
  (
     select
       id,name,age,sex,city,score,
       row_number()  over(partition by city order by score desc) as rn
     from t
  ) o
where rn<=2

再使用DSL风格的API实现,示例代码如下:
代码10-9 DML_DSLAPI_WINDOW.scala

package chapter10

import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.expressions.Window

/**
 * author: yuhui
 * descriptions: 用dsl风格api实现sql中的窗口分析函数
 * date: 2024 - 12 - 02 4:39 下午
 */
object DML_DSLAPI_WINDOW {

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

    val spark: SparkSession = SparkSession.builder()
      .appName(this.getClass.getSimpleName)
      .master("local[*]")
      .getOrCreate()

    val df = spark.read.option("header", true).csv("BookData/input/10stu2.csv")

    import spark.implicits._
    import org.apache.spark.sql.functions._

    val window = Window.partitionBy(Symbol("city")).orderBy(Symbol("score").desc)

    df.select(Symbol("id"), Symbol("name"), Symbol("age"), Symbol("sex"), Symbol("city"), Symbol("score"), row_number().over(window) as "rn")
      .where(Symbol("rn") <= 2)
      .drop("rn") // 最后结果中不需要rn列,可以drop掉这个列
      .select("id", "name", "age", "sex", "city", "score") // 或者用select指定你所需要的列
      .show()
    spark.close()
  }
}

执行结果如图10-12所示。
在这里插入图片描述
Dataset提供与RDD类似的编程算子,即map、flatMap、reduceByKey等,不过本方式使用比较少见:
如果方便用SQL表达的逻辑,首选SQL。
如果不方便用SQL表达,则可以把Dataset转成RDD后使用RDD的算子。
直接在Dataset上调用类似RDD风格算子的代码示例如下:

/**
  * 四、 dataset/dataframe 调 RDD算子
  *
  * dataset调rdd算子,返回的还是dataset[U], 不过需要对应的 Encoder[U]
  *
  */

    val ds4: Dataset[(Int, String, Int)] = ds2.map(p => (p.id, p.name, p.age + 10))   // 元组有隐式Encoder自动传入

    val ds5: Dataset[JavaPerson] = ds2.map(p => new JavaPerson(p.id,p.name,p.age*2))(Encoders.bean(classOf[JavaPerson])) // Java类没有自动隐式Encoder,需要手动传

    val ds6: Dataset[Map[String, String]] = ds2.map(per => Map("name" -> per.name, "id" -> (per.id+""), "age" -> (per.age+"")))
    ds6.printSchema()
    /**
     * root
         |-- value: map (nullable = true)
         |    |-- key: string
         |    |-- value: string (valueContainsNull = true)
     */

    // 从ds6中查询每个人的姓名
    ds6.selectExpr("value['name']")

    // dataframe上调RDD算子,就等价于 dataset[Row]上调rdd算子
    val ds7: Dataset[(Int, String, Int)] = frame.map(row=>{
      val id: Int = row.getInt(0)
      val name: String = row.getAs[String]("name")
      val age: Int = row.getAs[Int]("age")
      (id,name,age)
    })
    
    // 利用模式匹配从row中抽取字段数据
    val ds8: Dataset[Per] = frame.map({
      case Row(id:Int,name:String,age:Int) => Per(id,name,age*10)
    })

10.3 DataFrame输出

DataFrame在Spark中支持多种输出方式:可直接输出到控制台进行快速查看,也能保存为文件以便持久化存储。此外,它还能将数据写入RDBMS和Hive,并支持在输出时进行分区操作以优化性能。

10.3.1 输出控制台

df.show()
df.show(10)        //输出10行  
df.show(10,false)  // 不要截断列

10.3.2 输出文件

object SaveDF {
  def main(args: Array[String]): Unit = {
    val spark = SparkUtil.getSpark()
    val df = spark.read.option("header",true).csv("data/stu2.csv")
    val res = df.where("id>3").select("id","name")
    // 展示结果
    res.show(10,false)
}

保存结果为文件,格式包括parquet、json、csv、orc、text。文本文件是自由格式,框架无法判断该输出什么样的形式。示例如下:

res.write.parquet("out/parquetfile/")​
res.write.csv("out/csvfile")​
res.write.orc("out/orcfile")​
res.write.json("out/jsonfile")

要将df输出为普通文本文件,则需要将df变成一个列:

res.selectExpr("concat_ws('\001',id,name)")
  .write.text("out/textfile")

10.3.3 输出到RDBMS

将dataframe写入MySQL的表:

// 将dataframe通过jdbc写入mysql
val props = new Properties()
props.setProperty("user","root")
props.setProperty("password","yuhui888")
// 可以通过SaveMode来控制写入模式:
aveMode.Append/Ignore/Overwrite/ErrorIfExists(默认)
res.write.mode(SaveMode.Append).jdbc("jdbc:mysql://yuhui01:3306/spark?characterEncoding=utf8","res",props)

10.3.4 输出到Hive

开启Spark的Hive支持,代码如下:

val spark = SparkSession
  .builder()
  .appName("")
  .master("local[*]")
  .enableHiveSupport()
  .getOrCreate()

需要在工程中添加hive-site.xml、core-site.xml、hdfs-site.xml配置文件,如图10-13所示。
在这里插入图片描述
编写代码如下:

// 将dataframe写入Hive,saveAsTable就是保存为Hive的表
// 前提,Spark要开启hiveSupport支持,spark-hive的依赖,Hive的配置文件
res.write.saveAsTable("res")

10.3.5 DF输出时的分区操作

文件名称为04-dept-area7.txt,数据如下:

10,武汉市,hubei,2024-07-01
20,咸宁市,hubei,2024-07-01
30,赤壁市,hubei,2024-07-01
40,赤马港,hubei,2024-07-01

文件名称为04-dept-area8.txt,数据如下:

10,海淀区,beijing,2024-08-01
20,朝阳区,beijing,2024-08-01
30,东城区,beijing,2024-08-01
40,西城区,beijing,2024-08-01

文件名称为04-dept-area9.txt,数据如下:

10,宝安区,shenzhen,2024-09-01
20,南山区,shenzhen,2024-09-01
30,福田区,shenzhen,2024-09-01
40,龙岗区,shenzhen,2024-09-01

将数据上传到HDFS上的/hive_data路径下面,通过Hive进行加载。Hive命令如下:

create table tx(id int,name string) partitioned by (city string);
load data inpath '/hive_data/04-dept-area7.txt' into table tx partition(city="hubei");
load data inpath '/hive_data/04-dept-area8.txt' into table tx partition(city="beijing");
load data inpath '/hive_data/04-dept-area9.txt' into table tx partition(city="shenzheng");

Hive的表tx的目录结构如下:

/user/hive/warehouse/tx/
                    city=hubei/04-dept-area7.txt
                    city=beijing/04-dept-area8.txt
                    city=shenzheng/04-dept-area9.txt

查询的时候,分区标识字段,可以看做表的一个字段来用:

select * from  tx  where city='hubei'

Hive的表是建立HDFS上面,Hive分区表在HDFS的展示图,如图10-14所示。
在这里插入图片描述
通过Spark SQL查询Hive的数据,Spark SQL命令如下,展示结果如图10-15。

/**
  * sparksql对分区机制的支持
  * 识别已存在分区结构  /tx/city=hubei; /tx/city=beijing; /tx/city=shenzheng;
  * 会将所有子目录都理解为数据内容,会将子目录中的city理解为一个字段
  */
spark.read.csv("/user/hive/warehouse/tx").show()

在这里插入图片描述
通过Spark SQL能将数据按分区机制输出:

/**
  * sparksql对分区机制的支持,将dataframe存储为分区结构
  */
val dfp = spark.read.option("header",true).csv("/user/hive/warehouse/tx")
dfp.write.partitionBy("city").csv("/user/hive/warehouse/tx1")

输入结构如图10-16所示。
在这里插入图片描述

本书其他章节

  1. Spark大数据开发与应用案例(视频教学版)(一)–文前
  2. Spark大数据开发与应用案例(视频教学版)(二)–第一章上
  3. Spark大数据开发与应用案例(视频教学版)(三)–第一章下
  4. Spark大数据开发与应用案例(视频教学版)(四)–第二章上
  5. Spark大数据开发与应用案例(视频教学版)(五)–第二章下
  6. Spark大数据开发与应用案例(视频教学版)(六)–第三章上
  7. Spark大数据开发与应用案例(视频教学版)(七)–第三章下
  8. Spark大数据开发与应用案例(视频教学版)(八)–第四章上
  9. Spark大数据开发与应用案例(视频教学版)(九)–第四章下
  10. Spark大数据开发与应用案例(视频教学版)(十)–第五章
  11. Spark大数据开发与应用案例(视频教学版)(十一)–第六章
  12. Spark大数据开发与应用案例(视频教学版)(十二)–第七章
  13. Spark大数据开发与应用案例(视频教学版)(十三)–第八章
  14. Spark大数据开发与应用案例(视频教学版)(十四)–第九章

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

辉哥大数据

你的鼓舞将是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值