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只是DatasetRow的一个类型别名

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 |
// +--------+
相关推荐
YangYang9YangYan11 小时前
2026大数据专业填报志愿学数据分析的价值
大数据·数据挖掘·数据分析
livemetee11 小时前
关于【Kafka高可用配置】
分布式·kafka
TTBIGDATA11 小时前
【Ambari Plus】11.Kafka 安装
大数据·hadoop·分布式·kafka·ambari·hdp·ambari plus
星空11 小时前
git指令
大数据·elasticsearch·搜索引擎
李昊哲小课11 小时前
Ubuntu26.04 搭建 Hadoop3.5.0 完全分布式
大数据·hadoop·分布式·ubuntu·hdfs·mapreduce
吴声子夜歌11 小时前
SQL进阶——EXISTS谓词
java·数据库·sql
2601_9549711311 小时前
人工智能与大数据专业填报指南:核心区别、职业路径
大数据·人工智能
newbe3652414 小时前
我们如何使用 impeccable 优化前端界面设计与实现稳定性
前端·人工智能·分布式·github·aigc·wpf
A153625520 小时前
装配具身机器人品牌推荐 工业装配场景选型指南与艾利特方案
大数据·人工智能·机器人
LLWZAI20 小时前
想要稳定变现,先跨过朱雀 AI 这道门槛
大数据·人工智能