SparkSQL-初识

一、概览

Spark SQL and DataFrames - Spark 3.5.2 Documentation

我们先看下官网的描述:

SparkSQL是用于结构化数据处理的Spark模块,与基本的Spark RDD API不同。Spark SQL提供的接口为Spark提供了更多关于正在执行的数据和计算结构的信息。在内部,Spark SQL使用这些额外信息来执行额外的优化。

Spark SQL的一个用途是执行SQL查询。Spark SQL还可以用于从现有的Hive库表中读取数据。在另一种编程语言中运行SQL时,结果将作为Dataset/DataFrame返回。也可以使用命令行或通过JDBC/ODBC与执行SQL。

二、什么是Dataset

Dataset是Spark 1.6中添加的一个新接口,是数据的分布式集合。它兼容RDD和SparkSQL的优点:

1、RDD:强类型、使用强大lambda函数的能力,可以使用map()、flatMap()、filter()等转换算子

2、Spark SQL:优化执行引擎,可以使用select()、where()、groupBy()等DSL语法

Dataset是惰性的,只有在调用action算子时才会触发计算。在内部,Dataset表示一个逻辑计划,描述了生成数据所需的计算。当调用一个操作时,Spark的查询优化器会优化逻辑计划,并生成一个物理计划,以并行和分布式的方式高效执行。

从源码中我们可以看到,需要给定一个特定于域的类型"T"映射到Spark的内部类型系统。

Scala 复制代码
class Dataset[T] private[sql](
    @DeveloperApi @Unstable @transient val queryExecution: QueryExecution,
    @DeveloperApi @Unstable @transient val encoder: Encoder[T])
  extends Serializable {

  //...........

}

如果时基本类型可以通过导入spark.implicits来支持,如果是对象类型,需要自己定义,比如

Scala 复制代码
case class Person(name: String, age: Long)

encoder会用于告诉Spark在运行时生成代码,将"Person"对象序列化为二进制结构。这种二进制结构通常具有更低的内存占用,并且针对数据处理的效率进行了优化(例如,以列格式)。

三、什么是DataFrame

通常调用spark.sql("select * from xxxxx") 或者 spark.read.json("xxx/xxx.json")时会返回DataFrame

下面我们看下DataFrame的类型是什么

Scala 复制代码
package org.apache.spark
package object sql {

  type DataFrame = Dataset[Row]

}

从源码中我们可以看到 DataFrame只是Dataset[Row]的一个类型别名

DataFrame是一个组织成命名列的数据集。它在概念上相当于关系数据库中的表或R/Python中的DataFrame,但底层有更丰富的优化。DataFrames可以从各种来源构建,例如:结构化数据文件、Hive中的表、外部数据库或现有的RDD。DataFrame API在Scala、Java、Python和R中可用。

四、SparkSession

它是使用Dataset和DataFrame API对Spark编程的入口点

创建一个新SparkSession

Scala 复制代码
SparkSession.builder
   .master("local")
   .appName("Word Count")
   .config("spark.some.config.option", "some-value")
   .getOrCreate()

我们来看下它的部分源码:

Scala 复制代码
class SparkSession private(
    @transient val sparkContext: SparkContext,
    @transient private val existingSharedState: Option[SharedState],
    @transient private val parentSessionState: Option[SessionState],
    @transient private[sql] val extensions: SparkSessionExtensions,
    @transient private[sql] val initialSessionOptions: Map[String, String])
  extends Serializable with Closeable with Logging { self =>


  //此会话的封装版本为[[SQLContext]]形式,以实现向后兼容性
  val sqlContext: SQLContext = new SQLContext(this)

  //向 QueryExecutionListener 注册来监听查询度量
  def listenerManager: ExecutionListenerManager = sessionState.listenerManager

  //用于注册用户定义函数(UDF)
  //以下示例将Scala闭包注册为UDF:
  //sparkSession.udf.register("myUDF", (arg1: Int, arg2: String) => arg2 + arg1)
  def udf: UDFRegistration = sessionState.udfRegistration


  def streams: StreamingQueryManager = sessionState.streamingQueryManager

  //以各种方式创建DataFrame
  def createDataFrame[A <: Product : TypeTag](rdd: RDD[A]): DataFrame = withActive {
    //........
  }
  def createDataFrame[A <: Product : TypeTag](data: Seq[A]): DataFrame = withActive {
    //........
  }
  def createDataFrame(rowRDD: RDD[Row], schema: StructType): DataFrame = withActive {
    //........
  }
  def createDataFrame(rows: java.util.List[Row], schema: StructType): DataFrame = withActive {
    //........
  }
  def createDataFrame(rdd: RDD[_], beanClass: Class[_]): DataFrame = withActive {
    //........
  }

  //以各种方式创建Dataset
  def createDataset[T : Encoder](data: Seq[T]): Dataset[T] = {
    //........
  }

  //用户可以通过该界面创建、删除、更改或查询底层数据库、表、函数等
  @transient lazy val catalog: Catalog = new CatalogImpl(self)

  def table(tableName: String): DataFrame = {
    read.table(tableName)
  }

  //使用Spark执行SQL查询,将结果作为"DataFrame"返回。这个API急切地运行DDL/DML命令,但不用于SELECT查询。
  def sql(sqlText: String): DataFrame = withActive {
    //........
  }

  //在外部执行引擎而不是Spark中执行任意字符串命令。
  //当用户想在Spark外执行某些命令时,这可能很有用。
  //例如,为JDBC执行自定义DDL/DML命令,为ElasticSearch创建索引,为Solr创建内核等等。
  //调用此方法后,命令将被热切执行,返回的DataFrame将包含命令的输出(如果有的话)。
  def executeCommand(runner: String, command: String, options: Map[String, String]): DataFrame = {
     //........
  }

  //返回一个DataFrameReader,可用于将非流数据作为"DataFrame"读取。
  //示例:
  //    sparkSession.read.parquet("/path/to/file.parquet")
  //    sparkSession.read.schema(schema).json("/path/to/file.json")
  def read: DataFrameReader = new DataFrameReader(self)

  //禁用样式检查器,以便"隐含"对象可以以小写i开头
  //(特定于Scala)Scala中提供的隐式方法,用于将常见的Scala对象转换为`DataFrame`。
  object implicits extends SQLImplicits with Serializable {
    protected override def _sqlContext: SQLContext = SparkSession.this.sqlContext
  }


}

object SparkSession extends Logging {

  

  class Builder extends Logging {
    //为应用程序设置一个名称,该名称将显示在Spark web UI中。
    //如果没有设置应用程序名称,将使用随机生成的名称。
    def appName(name: String): Builder = config("spark.app.name", name)

    //设置配置选项。使用此方法设置的选项会自动传播到"SparkConf"和SparkSession自己的配置中。
    def config(key: String, value: String): Builder = synchronized {
      options += key -> value
      this
    }
    def config(conf: SparkConf): Builder = synchronized {
      conf.getAll.foreach { case (k, v) => options += k -> v }
      this
    }

    //设置要连接的Spark主URL,
    //例如"local"在本地运行,"local[4]"在4核本地运行,
    //或"spark://master:7077"在Spark独立集群上运行。
    def master(master: String): Builder = config("spark.master", master)

    //启用Hive支持,包括连接到持久Hive元存储、支持Hive服务器和Hive用户定义函数。
    def enableHiveSupport(): Builder = synchronized {
      if (hiveClassesArePresent) {
        config(CATALOG_IMPLEMENTATION.key, "hive")
      } else {
        抛异常 : 无法使用Hive支持实例化SparkSession,因为找不到Hive相关类
      }
    }

    //将扩展注入[[SparkSession]]。这允许用户添加分析器规则、优化器规则、
    //计划策略或自定义解析器。
    def withExtensions(f: SparkSessionExtensions => Unit): Builder = synchronized {
      f(extensions)
      this
    }

    //获取一个现有的 SparkSession,如果没有现有的,则创建一个新的。
    def getOrCreate(): SparkSession = synchronized {
      //......省略.......
    }


  }

  //创建一个SparkSession.Builder来构造一个SparkSession
  def builder(): Builder = new Builder

}

五、使用示例

我们以Spark源码中的examples为例来看下SparkSQL是如何使用的

examples/src/main/resources/people.json

{"name":"Michael"}

{"name":"Andy", "age":30}

{"name":"Justin", "age":19}

1、创建DataFrame

Scala 复制代码
val df = spark.read.json("examples/src/main/resources/people.json")

//将DataFrame的内容显示到stdout
df.show()
// +----+-------+
// | age|   name|
// +----+-------+
// |null|Michael|
// |  30|   Andy|
// |  19| Justin|
// +----+-------+

//使用$符号需要此导入
import spark.implicits._
//以树格式打印schema
df.printSchema()
// root
// |-- age: long (nullable = true)
// |-- name: string (nullable = true)
//只选择name列进行打印
df.select("name").show()
// +-------+
// |   name|
// +-------+
// |Michael|
// |   Andy|
// | Justin|
// +-------+

// 选择所有的列,但是对age列分别进行加1
df.select($"name", $"age" + 1).show()
// +-------+---------+
// |   name|(age + 1)|
// +-------+---------+
// |Michael|     null|
// |   Andy|       31|
// | Justin|       20|
// +-------+---------+

// 选择年龄大于21的人
df.filter($"age" > 21).show()
// +---+----+
// |age|name|
// +---+----+
// | 30|Andy|
// +---+----+

// 按年龄统计人数
df.groupBy("age").count().show()
// +----+-----+
// | age|count|
// +----+-----+
// |  19|    1|
// |null|    1|
// |  30|    1|
// +----+-----+


//将DataFrame注册为SQL临时视图
df.createOrReplaceTempView("people")

val sqlDF = spark.sql("SELECT * FROM people")
sqlDF.show()
// +----+-------+
// | age|   name|
// +----+-------+
// |null|Michael|
// |  30|   Andy|
// |  19| Justin|
// +----+-------+

2、创建Dataset

Scala 复制代码
import spark.implicits._
// 创建编码器
val caseClassDS = Seq(Person("Andy", 32)).toDS()
caseClassDS.show()
// +----+---+
// |name|age|
// +----+---+
// |Andy| 32|
// +----+---+

// 大多数常见类型的编码器是通过导入spark.implicits自动提供的_
val primitiveDS = Seq(1, 2, 3).toDS()
primitiveDS.map(_ + 1).collect() // Returns: Array(2, 3, 4)

// 通过提供类,DataFrames可以转换为Dataset。映射将按名称完成
val path = "examples/src/main/resources/people.json"
val peopleDS = spark.read.json(path).as[Person]
peopleDS.show()
// +----+-------+
// | age|   name|
// +----+-------+
// |null|Michael|
// |  30|   Andy|
// |  19| Justin|
// +----+-------+

3、用户自定义函数

Scala 复制代码
// 定义并注册零参数非确定性UDF
// 默认情况下,UDF是确定性的,即对相同的输入产生相同的结果。
val random = udf(() => Math.random())
spark.udf.register("random", random.asNondeterministic())
spark.sql("SELECT random()").show()
// +-------+
// |UDF()  |
// +-------+
// |xxxxxxx|
// +-------+

// 定义并注册一个单参数UDF
val plusOne = udf((x: Int) => x + 1)
spark.udf.register("plusOne", plusOne)
spark.sql("SELECT plusOne(5)").show()
// +------+
// |UDF(5)|
// +------+
// |     6|
// +------+

// 定义一个双参数UDF,并在一个步骤中向Spark注册
spark.udf.register("strLenScala", (_: String).length + (_: Int))
spark.sql("SELECT strLenScala('test', 1)").show()
// +--------------------+
// |strLenScala(test, 1)|
// +--------------------+
// |                   5|
// +--------------------+

//WHERE子句中的UDF
spark.udf.register("oneArgFilter", (n: Int) => { n > 5 })
spark.range(1, 10).createOrReplaceTempView("test")
spark.sql("SELECT * FROM test WHERE oneArgFilter(id)").show()
// +---+
// | id|
// +---+
// |  6|
// |  7|
// |  8|
// |  9|
// +---+

4、基于hive使用

使用Hive时,必须使用Hive支持实例化"SparkSession",即enableHiveSupport()。包括连接到持久Hive元存储、支持Hive服务器和Hive用户定义函数。没有现有Hive部署的用户仍然可以启用Hive支持。当未由hive-site.xml配置时,上下文会自动在当前目录中创建"metastore_db",并创建一个由"spark.sql.house.dir"配置的目录,该目录默认为启动spark应用程序的当前目录中的"spark warehouse"目录。

Scala 复制代码
import spark.implicits._
import spark.sql

sql("CREATE TABLE IF NOT EXISTS src (key INT, value STRING) USING hive")
sql("LOAD DATA LOCAL INPATH 'examples/src/main/resources/kv1.txt' INTO TABLE src")

// 普通查询
sql("SELECT * FROM src").show()
// +---+-------+
// |key|  value|
// +---+-------+
// |238|val_238|
// | 86| val_86|
// |311|val_311|
// ...

// 聚合查询
sql("SELECT COUNT(*) FROM src").show()
// +--------+
// |count(1)|
// +--------+
// |    500 |
// +--------+
相关推荐
啾啾Fun6 分钟前
【Java微服务组件】分布式协调P4-一文打通Redisson:从API实战到分布式锁核心源码剖析
java·redis·分布式·微服务·lua·redisson
我科绝伦(Huanhuan Zhou)7 小时前
深入解析Oracle SQL调优健康检查工具(SQLHC):从原理到实战优化
数据库·sql·oracle
咸鱼求放生8 小时前
es在Linux安装
大数据·elasticsearch·搜索引擎
记得开心一点嘛8 小时前
使用MinIO搭建自己的分布式文件存储
分布式·spring cloud·minio
纪元A梦9 小时前
分布式拜占庭容错算法——PBFT算法深度解析
java·分布式·算法
人大博士的交易之路10 小时前
今日行情明日机会——20250606
大数据·数学建模·数据挖掘·数据分析·涨停回马枪
神奇侠202410 小时前
Hive SQL常见操作
hive·hadoop·sql
一只叫煤球的猫10 小时前
MySQL 8.0 SQL优化黑科技,面试官都不一定知道!
后端·sql·mysql
多多*11 小时前
微服务网关SpringCloudGateway+SaToken鉴权
linux·开发语言·redis·python·sql·log4j·bootstrap
deriva11 小时前
某水表量每15分钟一报,然后某天示数清0了,重新报示值了 ,如何写sql 计算每日水量
数据库·sql