当前位置: 首页 > news >正文

Spark-3.5.7文档3 - Spark SQL、DataFrame 和 Dataset 指南

Spark SQL 是用于结构化数据处理的 Spark 模块。与基础的 Spark RDD API 不同,Spark SQL 提供的接口为 Spark 提供了关于数据结构和正在执行的计算的更多信息。在内部,Spark SQL 利用这些额外信息执行额外的优化。与 Spark SQL 交互的方式有多种,包括 SQL 和 Dataset API。在计算结果时,使用的是相同的执行引擎,与您使用哪种 API/语言来表达计算无关。这种统一意味着开发人员可以根据哪种方式能最自然地表达给定的转换,轻松地在不同 API 之间切换。

本文档中的所有示例均使用 Spark 发行版中包含的示例数据,并可以在 spark-shell、pyspark shell 或 sparkR shell 中运行。

SQL

Spark SQL 的一种用途是执行 SQL 查询。Spark SQL 也可用于从现有的 Hive 安装中读取数据。有关如何配置此功能的更多信息,请参阅 Hive 表 部分。在另一种编程语言中运行 SQL 时,结果将作为 Dataset/DataFrame 返回。您还可以使用命令行或通过 JDBC/ODBC 与 SQL 接口进行交互。

Dataset 和 DataFrame

Dataset 是一个分布式的数据集合。Dataset 是 Spark 1.6 中添加的一个新接口,它既提供了 RDD 的优势(强类型化,能够使用强大的 lambda 函数),又具备了 Spark SQL 优化执行引擎的优势。Dataset 可以从 JVM 对象构建,然后使用函数式转换(map、flatMap、filter 等)进行操作。Dataset API 在 Scala 和 Java 中可用。Python 不支持 Dataset API。但由于 Python 的动态特性,Dataset API 的许多优势已经可用(例如,您可以通过名称自然地访问行的字段 row.columnName)。R 语言的情况类似。

DataFrame 是被组织成命名列的 Dataset。它在概念上等同于关系数据库中的表或 R/Python 中的数据框,但在底层具有更丰富的优化。DataFrame 可以从多种来源构建,例如:结构化数据文件、Hive 中的表、外部数据库或现有的 RDD。DataFrame API 在 Scala、Java、Python 和 R 中可用。在 Scala 和 Java 中,DataFrame 由 Row 的 Dataset 表示。在 Scala API 中,DataFrame 只是 Dataset[Row] 的类型别名。而在 Java API 中,用户需要使用 Dataset 来表示 DataFrame。

在本文档中,我们通常将 Scala/Java 中 Row 的 Dataset 称为 DataFrame。

入门

Python 方式

起点:SparkSession

Spark中所有功能的入口点是SparkSession类。要创建一个基础的SparkSession,只需使用SparkSession.builder:

from pyspark.sql import SparkSessionspark = SparkSession \.builder \.appName("Python Spark SQL基础示例") \.config("spark.some.config.option", "some-value") \.getOrCreate()

完整示例代码可在Spark代码库的"examples/src/main/python/sql/basic.py"中找到。

Spark 2.0中的SparkSession内置支持Hive功能,包括使用HiveQL编写查询、访问Hive UDF以及从Hive表读取数据的能力。使用这些功能时,无需预先设置Hive环境。

创建DataFrame

通过SparkSession,应用程序可以从现有RDD、Hive表或Spark数据源创建DataFrame。

例如,以下代码基于JSON文件内容创建DataFrame:

# spark是已存在的SparkSession
df = spark.read.json("examples/src/main/resources/people.json")
# 将DataFrame内容输出到标准输出
df.show()
# +----+-------+
# | age|   name|
# +----+-------+
# |null|Michael|
# |  30|   Andy|
# |  19| Justin|
# +----+-------+

完整示例代码可在Spark代码库的"examples/src/main/python/sql/basic.py"中找到。

无类型数据集操作(又称DataFrame操作)

DataFrame为Scala、Java、Python和R中的结构化数据操作提供了一种领域特定语言。

如上所述,在Spark 2.0中,DataFrame在Scala和Java API中只是Row对象的Dataset。这些操作也称为"非类型化转换",与强类型Scala/Java Dataset的"类型化转换"相对。

以下是一些使用Dataset进行结构化数据处理的基础示例:

在Python中,可以通过属性(df.age)或索引(df[‘age’])访问DataFrame的列。虽然前者便于交互式数据探索,但强烈建议使用后者形式,这种方式具有未来兼容性,且不会因列名与DataFrame类的属性冲突而出现问题。

# spark和df来自前面的示例
# 以树形格式打印模式
df.printSchema()
# root
# |-- age: long (nullable = true)
# |-- name: string (nullable = true)# 仅选择"name"列
df.select("name").show()
# +-------+
# |   name|
# +-------+
# |Michael|
# |   Andy|
# | Justin|
# +-------+# 选择所有人,但年龄加1
df.select(df['name'], df['age'] + 1).show()
# +-------+---------+
# |   name|(age + 1)|
# +-------+---------+
# |Michael|     null|
# |   Andy|       31|
# | Justin|       20|
# +-------+---------+# 选择年龄大于21岁的人
df.filter(df['age'] > 21).show()
# +---+----+
# |age|name|
# +---+----+
# | 30|Andy|
# +---+----+# 按年龄统计人数
df.groupBy("age").count().show()
# +----+-----+
# | age|count|
# +----+-----+
# |  19|    1|
# |null|    1|
# |  30|    1|
# +----+-----+

完整示例代码可在Spark代码库的"examples/src/main/python/sql/basic.py"中找到。

有关可在DataFrame上执行的操作类型的完整列表,请参阅API文档。

除了简单的列引用和表达式外,DataFrame还有一个丰富的函数库,包括字符串操作、日期算术、常见数学运算等。完整列表可在DataFrame函数参考中找到。

以编程方式运行SQL查询

SparkSession上的sql函数使应用程序能够以编程方式运行SQL查询,并将结果作为DataFrame返回。

# 将DataFrame注册为SQL临时视图
df.createOrReplaceTempView("people")sqlDF = spark.sql("SELECT * FROM people")
sqlDF.show()
# +----+-------+
# | age|   name|
# +----+-------+
# |null|Michael|
# |  30|   Andy|
# |  19| Justin|
# +----+-------+

完整示例代码可在Spark代码库的"examples/src/main/python/sql/basic.py"中找到。

全局临时视图

Spark SQL中的临时视图是会话范围的,如果创建它的会话终止,视图将消失。如果想要一个在所有会话之间共享并在Spark应用程序终止之前保持存活的临时视图,可以创建全局临时视图。全局临时视图绑定到系统保留的数据库global_temp,必须使用限定名称来引用它,例如SELECT * FROM global_temp.view1。

# 将DataFrame注册为全局临时视图
df.createGlobalTempView("people")# 全局临时视图绑定到系统保留的数据库`global_temp`
spark.sql("SELECT * FROM global_temp.people").show()
# +----+-------+
# | age|   name|
# +----+-------+
# |null|Michael|
# |  30|   Andy|
# |  19| Justin|
# +----+-------+# 全局临时视图是跨会话的
spark.newSession().sql("SELECT * FROM global_temp.people").show()
# +----+-------+
# | age|   name|
# +----+-------+
# |null|Michael|
# |  30|   Andy|
# |  19| Justin|
# +----+-------+

完整示例代码可在Spark代码库的"examples/src/main/python/sql/basic.py"中找到。

创建Dataset

Dataset与RDD类似,但它们使用专门的Encoder来序列化对象以进行处理或通过网络传输,而不是使用Java序列化或Kryo。虽然编码器和标准序列化都负责将对象转换为字节,但编码器是动态生成的代码,并使用一种格式,允许Spark执行许多操作(如过滤、排序和哈希)而无需将字节反序列化回对象。

case class Person(name: String, age: Long)// 为case类创建编码器
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() // 返回: Array(2, 3, 4)// 可以通过提供类将DataFrame转换为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|
// +----+-------+

完整示例代码可在Spark代码库的"examples/src/main/scala/org/apache/spark/examples/sql/SparkSQLExample.scala"中找到。

与RDD交互操作

Spark SQL支持两种不同的方法将现有RDD转换为Dataset。第一种方法使用反射来推断包含特定类型对象的RDD的模式。这种基于反射的方法使代码更简洁,在编写Spark应用程序时已经知道模式的情况下效果很好。

创建Dataset的第二种方法是通过编程接口,允许您构建模式,然后将其应用于现有RDD。虽然这种方法更繁琐,但它允许在运行时才知道列及其类型时构建Dataset。

使用反射推断模式

Spark SQL可以将Row对象的RDD转换为DataFrame,并推断数据类型。Row通过将键/值对列表作为kwargs传递给Row类来构造。此列表的键定义表的列名,类型通过采样整个数据集来推断,类似于对JSON文件执行的推断。

from pyspark.sql import Rowsc = spark.sparkContext# 加载文本文件并将每行转换为Row
lines = sc.textFile("examples/src/main/resources/people.txt")
parts = lines.map(lambda l: l.split(","))
people = parts.map(lambda p: Row(name=p[0], age=int(p[1])))# 推断模式,并将DataFrame注册为表
schemaPeople = spark.createDataFrame(people)
schemaPeople.createOrReplaceTempView("people")# 可以在已注册为表的DataFrame上运行SQL
teenagers = spark.sql("SELECT name FROM people WHERE age >= 13 AND age <= 19")# SQL查询的结果是Dataframe对象
# rdd以:class:`pyspark.RDD` of :class:`Row`的形式返回内容
teenNames = teenagers.rdd.map(lambda p: "Name: " + p.name).collect()
for name in teenNames:print(name)
# Name: Justin

完整示例代码可在Spark代码库的"examples/src/main/python/sql/basic.py"中找到。

以编程方式指定模式

当无法提前定义kwargs字典时(例如,记录的结构编码在字符串中,或者文本数据集将被解析并且字段将针对不同用户进行不同投影),可以通过三个步骤以编程方式创建DataFrame:

  • 从原始RDD创建元组或列表的RDD;
  • 创建与步骤1中创建的RDD中的元组或列表结构匹配的StructType表示的模式;
  • 通过SparkSession提供的createDataFrame方法将模式应用于RDD。

例如:

# 导入数据类型
from pyspark.sql.types import StringType, StructType, StructFieldsc = spark.sparkContext# 加载文本文件并将每行转换为Row
lines = sc.textFile("examples/src/main/resources/people.txt")
parts = lines.map(lambda l: l.split(","))
# 每行转换为元组
people = parts.map(lambda p: (p[0], p[1].strip()))# 模式编码在字符串中
schemaString = "name age"fields = [StructField(field_name, StringType(), True) for field_name in schemaString.split()]
schema = StructType(fields)# 将模式应用于RDD
schemaPeople = spark.createDataFrame(people, schema)# 使用DataFrame创建临时视图
schemaPeople.createOrReplaceTempView("people")# 可以在已注册为表的DataFrame上运行SQL
results = spark.sql("SELECT name FROM people")results.show()
# +-------+
# |   name|
# +-------+
# |Michael|
# |   Andy|
# | Justin|
# +-------+

完整示例代码可在Spark代码库的"examples/src/main/python/sql/basic.py"中找到。

标量函数

标量函数是每行返回单个值的函数,与聚合函数(为行组返回一个值)相对。Spark SQL支持多种内置标量函数,也支持用户定义标量函数。

聚合函数

聚合函数是在行组上返回单个值的函数。内置聚合函数提供常见的聚合,如count()、count_distinct()、avg()、max()、min()等。用户不限于预定义的聚合函数,可以创建自己的聚合函数。有关用户定义聚合函数的更多详细信息,请参阅用户定义聚合函数的文档。

Scala 方式

起点:SparkSession

Spark中所有功能的入口点是SparkSession类。要创建一个基础的SparkSession,只需使用SparkSession.builder():

import org.apache.spark.sql.SparkSessionval spark = SparkSession.builder().appName("Spark SQL基础示例").config("spark.some.config.option", "some-value").getOrCreate()

完整示例代码可在Spark代码库的"examples/src/main/scala/org/apache/spark/examples/sql/SparkSQLExample.scala"中找到。

Spark 2.0中的SparkSession内置支持Hive功能,包括使用HiveQL编写查询、访问Hive UDF以及从Hive表读取数据的能力。使用这些功能时,无需预先设置Hive环境。

创建DataFrame

通过SparkSession,应用程序可以从现有RDD、Hive表或Spark数据源创建DataFrame。

例如,以下代码基于JSON文件内容创建DataFrame:

val df = spark.read.json("examples/src/main/resources/people.json")// 将DataFrame内容输出到标准输出
df.show()
// +----+-------+
// | age|   name|
// +----+-------+
// |null|Michael|
// |  30|   Andy|
// |  19| Justin|
// +----+-------+

完整示例代码可在Spark代码库的"examples/src/main/scala/org/apache/spark/examples/sql/SparkSQLExample.scala"中找到。

无类型数据集操作(又称DataFrame操作)

DataFrame为Scala、Java、Python和R中的结构化数据操作提供了一种领域特定语言。

如上所述,在Spark 2.0中,DataFrame在Scala和Java API中只是Row对象的Dataset。这些操作也称为"非类型化转换",与强类型Scala/Java Dataset的"类型化转换"相对。

以下是一些使用Dataset进行结构化数据处理的基础示例:

// 需要此导入来使用$-符号
import spark.implicits._
// 以树形格式打印模式
df.printSchema()
// root
// |-- age: long (nullable = true)
// |-- name: string (nullable = true)// 仅选择"name"列
df.select("name").show()
// +-------+
// |   name|
// +-------+
// |Michael|
// |   Andy|
// | Justin|
// +-------+// 选择所有人,但年龄加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|
// +----+-----+

完整示例代码可在Spark代码库的"examples/src/main/scala/org/apache/spark/examples/sql/SparkSQLExample.scala"中找到。

有关可在Dataset上执行的操作类型的完整列表,请参阅API文档。

除了简单的列引用和表达式外,Dataset还有一个丰富的函数库,包括字符串操作、日期算术、常见数学运算等。完整列表可在DataFrame函数参考中找到。

以编程方式运行SQL查询

SparkSession上的sql函数使应用程序能够以编程方式运行SQL查询,并将结果作为DataFrame返回。

// 将DataFrame注册为SQL临时视图
df.createOrReplaceTempView("people")val sqlDF = spark.sql("SELECT * FROM people")
sqlDF.show()
// +----+-------+
// | age|   name|
// +----+-------+
// |null|Michael|
// |  30|   Andy|
// |  19| Justin|
// +----+-------+

完整示例代码可在Spark代码库的"examples/src/main/scala/org/apache/spark/examples/sql/SparkSQLExample.scala"中找到。

全局临时视图

Spark SQL中的临时视图是会话范围的,如果创建它的会话终止,视图将消失。如果想要一个在所有会话之间共享并在Spark应用程序终止之前保持存活的临时视图,可以创建全局临时视图。全局临时视图绑定到系统保留的数据库global_temp,必须使用限定名称来引用它,例如SELECT * FROM global_temp.view1。

// 将DataFrame注册为全局临时视图
df.createGlobalTempView("people")// 全局临时视图绑定到系统保留的数据库`global_temp`
spark.sql("SELECT * FROM global_temp.people").show()
// +----+-------+
// | age|   name|
// +----+-------+
// |null|Michael|
// |  30|   Andy|
// |  19| Justin|
// +----+-------+// 全局临时视图是跨会话的
spark.newSession().sql("SELECT * FROM global_temp.people").show()
// +----+-------+
// | age|   name|
// +----+-------+
// |null|Michael|
// |  30|   Andy|
// |  19| Justin|
// +----+-------+

完整示例代码可在Spark代码库的"examples/src/main/scala/org/apache/spark/examples/sql/SparkSQLExample.scala"中找到。

创建数据集

Dataset与RDD类似,但它们使用专门的Encoder来序列化对象以进行处理或通过网络传输,而不是使用Java序列化或Kryo。虽然编码器和标准序列化都负责将对象转换为字节,但编码器是动态生成的代码,并使用一种格式,允许Spark执行许多操作(如过滤、排序和哈希)而无需将字节反序列化回对象。

case class Person(name: String, age: Long)// 为case类创建编码器
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() // 返回: Array(2, 3, 4)// 可以通过提供类将DataFrame转换为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|
// +----+-------+

完整示例代码可在Spark代码库的"examples/src/main/scala/org/apache/spark/examples/sql/SparkSQLExample.scala"中找到。

与RDD交互操作

Spark SQL支持两种不同的方法将现有RDD转换为Dataset。第一种方法使用反射来推断包含特定类型对象的RDD的模式。这种基于反射的方法使代码更简洁,在编写Spark应用程序时已经知道模式的情况下效果很好。

创建Dataset的第二种方法是通过编程接口,允许您构建模式,然后将其应用于现有RDD。虽然这种方法更繁琐,但它允许在运行时才知道列及其类型时构建Dataset。

使用反射推断模式

Spark SQL的Scala接口支持自动将包含case类的RDD转换为DataFrame。case类定义表的模式。case类参数的名称使用反射读取并成为列的名称。case类也可以嵌套或包含复杂类型,如Seq或Array。此RDD可以隐式转换为DataFrame,然后注册为表。表可用于后续的SQL语句。

// 用于从RDD到DataFrame的隐式转换
import spark.implicits._// 从文本文件创建Person对象的RDD,将其转换为DataFrame
val peopleDF = spark.sparkContext.textFile("examples/src/main/resources/people.txt").map(_.split(",")).map(attributes => Person(attributes(0), attributes(1).trim.toInt)).toDF()
// 将DataFrame注册为临时视图
peopleDF.createOrReplaceTempView("people")// 可以使用Spark提供的sql方法运行SQL语句
val teenagersDF = spark.sql("SELECT name, age FROM people WHERE age BETWEEN 13 AND 19")// 可以通过字段索引访问结果中行的列
teenagersDF.map(teenager => "Name: " + teenager(0)).show()
// +------------+
// |       value|
// +------------+
// |Name: Justin|
// +------------+// 或通过字段名访问
teenagersDF.map(teenager => "Name: " + teenager.getAs[String]("name")).show()
// +------------+
// |       value|
// +------------+
// |Name: Justin|
// +------------+// 没有为Dataset[Map[K,V]]预定义的编码器,需显式定义
implicit val mapEncoder = org.apache.spark.sql.Encoders.kryo[Map[String, Any]]
// 基本类型和case类也可以定义为
// implicit val stringIntMapEncoder: Encoder[Map[String, Any]] = ExpressionEncoder()// row.getValuesMap[T]一次将多列检索到Map[String, T]中
teenagersDF.map(teenager => teenager.getValuesMap[Any](List("name", "age"))).collect()
// Array(Map("name" -> "Justin", "age" -> 19))

完整示例代码可在Spark代码库的"examples/src/main/scala/org/apache/spark/examples/sql/SparkSQLExample.scala"中找到。

以编程方式指定模式

当无法提前定义case类时(例如,记录的结构编码在字符串中,或者文本数据集将被解析并且字段将针对不同用户进行不同投影),可以通过三个步骤以编程方式创建DataFrame:

  1. 从原始RDD创建Row的RDD;
  2. 创建与步骤1中创建的RDD中的Row结构匹配的StructType表示的模式;
  3. 通过SparkSession提供的createDataFrame方法将模式应用于Row的RDD。

例如:

import org.apache.spark.sql.Row
import org.apache.spark.sql.types._// 创建RDD
val peopleRDD = spark.sparkContext.textFile("examples/src/main/resources/people.txt")// 模式编码在字符串中
val schemaString = "name age"// 基于模式字符串生成模式
val fields = schemaString.split(" ").map(fieldName => StructField(fieldName, StringType, nullable = true))
val schema = StructType(fields)// 将RDD(people)的记录转换为Row
val rowRDD = peopleRDD.map(_.split(",")).map(attributes => Row(attributes(0), attributes(1).trim))// 将模式应用于RDD
val peopleDF = spark.createDataFrame(rowRDD, schema)// 使用DataFrame创建临时视图
peopleDF.createOrReplaceTempView("people")// 可以在使用DataFrame创建的临时视图上运行SQL
val results = spark.sql("SELECT name FROM people")// SQL查询的结果是DataFrame,支持所有正常的RDD操作
// 可以通过字段索引或字段名访问结果中行的列
results.map(attributes => "Name: " + attributes(0)).show()
// +-------------+
// |        value|
// +-------------+
// |Name: Michael|
// |   Name: Andy|
// | Name: Justin|
// +-------------+

完整示例代码可在Spark代码库的"examples/src/main/scala/org/apache/spark/examples/sql/SparkSQLExample.scala"中找到。

标量函数

标量函数是每行返回单个值的函数,与聚合函数(为行组返回一个值)相对。Spark SQL支持多种内置标量函数,也支持用户定义标量函数。

聚合函数

聚合函数是在行组上返回单个值的函数。内置聚合函数提供常见的聚合,如count()、count_distinct()、avg()、max()、min()等。用户不限于预定义的聚合函数,可以创建自己的聚合函数。有关用户定义聚合函数的更多详细信息,请参阅用户定义聚合函数的文档。

Java 方式

起点:SparkSession

Spark中所有功能的入口点是SparkSession类。要创建一个基础的SparkSession,只需使用SparkSession.builder():

import org.apache.spark.sql.SparkSession;SparkSession spark = SparkSession.builder().appName("Java Spark SQL基础示例").config("spark.some.config.option", "some-value").getOrCreate();

完整示例代码可在Spark代码库的"examples/src/main/java/org/apache/spark/examples/sql/JavaSparkSQLExample.java"中找到。

Spark 2.0中的SparkSession内置支持Hive功能,包括使用HiveQL编写查询、访问Hive UDF以及从Hive表读取数据的能力。使用这些功能时,无需预先设置Hive环境。

创建DataFrame

通过SparkSession,应用程序可以从现有RDD、Hive表或Spark数据源创建DataFrame。

例如,以下代码基于JSON文件内容创建DataFrame:

import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;Dataset<Row> df = spark.read().json("examples/src/main/resources/people.json");// 将DataFrame内容输出到标准输出
df.show();
// +----+-------+
// | age|   name|
// +----+-------+
// |null|Michael|
// |  30|   Andy|
// |  19| Justin|
// +----+-------+

完整示例代码可在Spark代码库的"examples/src/main/java/org/apache/spark/examples/sql/JavaSparkSQLExample.java"中找到。

无类型数据集操作(又称DataFrame操作)

DataFrame为Scala、Java、Python和R中的结构化数据操作提供了一种领域特定语言。

如上所述,在Spark 2.0中,DataFrame在Scala和Java API中只是Row对象的Dataset。这些操作也称为"非类型化转换",与强类型Scala/Java Dataset的"类型化转换"相对。

以下是一些使用Dataset进行结构化数据处理的基础示例:

// col("...") 比 df.col("...") 更可取
import static org.apache.spark.sql.functions.col;// 以树形格式打印模式
df.printSchema();
// root
// |-- age: long (nullable = true)
// |-- name: string (nullable = true)// 仅选择"name"列
df.select("name").show();
// +-------+
// |   name|
// +-------+
// |Michael|
// |   Andy|
// | Justin|
// +-------+// 选择所有人,但年龄加1
df.select(col("name"), col("age").plus(1)).show();
// +-------+---------+
// |   name|(age + 1)|
// +-------+---------+
// |Michael|     null|
// |   Andy|       31|
// | Justin|       20|
// +-------+---------+// 选择年龄大于21岁的人
df.filter(col("age").gt(21)).show();
// +---+----+
// |age|name|
// +---+----+
// | 30|Andy|
// +---+----+// 按年龄统计人数
df.groupBy("age").count().show();
// +----+-----+
// | age|count|
// +----+-----+
// |  19|    1|
// |null|    1|
// |  30|    1|
// +----+-----+

完整示例代码可在Spark代码库的"examples/src/main/java/org/apache/spark/examples/sql/JavaSparkSQLExample.java"中找到。

有关可在Dataset上执行的操作类型的完整列表,请参阅API文档。

除了简单的列引用和表达式外,Dataset还有一个丰富的函数库,包括字符串操作、日期算术、常见数学运算等。完整列表可在DataFrame函数参考中找到。

以编程方式运行SQL查询

SparkSession上的sql函数使应用程序能够以编程方式运行SQL查询,并将结果作为Dataset返回。

import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;// 将DataFrame注册为SQL临时视图
df.createOrReplaceTempView("people");Dataset<Row> sqlDF = spark.sql("SELECT * FROM people");
sqlDF.show();
// +----+-------+
// | age|   name|
// +----+-------+
// |null|Michael|
// |  30|   Andy|
// |  19| Justin|
// +----+-------+

完整示例代码可在Spark代码库的"examples/src/main/java/org/apache/spark/examples/sql/JavaSparkSQLExample.java"中找到。

全局临时视图

Spark SQL中的临时视图是会话范围的,如果创建它的会话终止,视图将消失。如果想要一个在所有会话之间共享并在Spark应用程序终止之前保持存活的临时视图,可以创建全局临时视图。全局临时视图绑定到系统保留的数据库global_temp,必须使用限定名称来引用它,例如SELECT * FROM global_temp.view1。

// 将DataFrame注册为全局临时视图
df.createGlobalTempView("people");// 全局临时视图绑定到系统保留的数据库`global_temp`
spark.sql("SELECT * FROM global_temp.people").show();
// +----+-------+
// | age|   name|
// +----+-------+
// |null|Michael|
// |  30|   Andy|
// |  19| Justin|
// +----+-------+// 全局临时视图是跨会话的
spark.newSession().sql("SELECT * FROM global_temp.people").show();
// +----+-------+
// | age|   name|
// +----+-------+
// |null|Michael|
// |  30|   Andy|
// |  19| Justin|
// +----+-------+

完整示例代码可在Spark代码库的"examples/src/main/java/org/apache/spark/examples/sql/JavaSparkSQLExample.java"中找到。

创建数据集

Dataset与RDD类似,但它们使用专门的Encoder来序列化对象以进行处理或通过网络传输,而不是使用Java序列化或Kryo。虽然编码器和标准序列化都负责将对象转换为字节,但编码器是动态生成的代码,并使用一种格式,允许Spark执行许多操作(如过滤、排序和哈希)而无需将字节反序列化回对象。

import java.util.Arrays;
import java.util.Collections;
import java.io.Serializable;import org.apache.spark.api.java.function.MapFunction;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
import org.apache.spark.sql.Encoder;
import org.apache.spark.sql.Encoders;public static class Person implements Serializable {private String name;private long age;public String getName() {return name;}public void setName(String name) {this.name = name;}public long getAge() {return age;}public void setAge(long age) {this.age = age;}
}// 创建Bean类的实例
Person person = new Person();
person.setName("Andy");
person.setAge(32);// 为Java bean创建编码器
Encoder<Person> personEncoder = Encoders.bean(Person.class);
Dataset<Person> javaBeanDS = spark.createDataset(Collections.singletonList(person),personEncoder
);
javaBeanDS.show();
// +---+----+
// |age|name|
// +---+----+
// | 32|Andy|
// +---+----+// 大多数常见类型的编码器在Encoders类中提供
Encoder<Long> longEncoder = Encoders.LONG();
Dataset<Long> primitiveDS = spark.createDataset(Arrays.asList(1L, 2L, 3L), longEncoder);
Dataset<Long> transformedDS = primitiveDS.map((MapFunction<Long, Long>) value -> value + 1L,longEncoder);
transformedDS.collect(); // 返回 [2, 3, 4]// 可以通过提供类将DataFrame转换为Dataset。基于名称进行映射
String path = "examples/src/main/resources/people.json";
Dataset<Person> peopleDS = spark.read().json(path).as(personEncoder);
peopleDS.show();
// +----+-------+
// | age|   name|
// +----+-------+
// |null|Michael|
// |  30|   Andy|
// |  19| Justin|
// +----+-------+

完整示例代码可在Spark代码库的"examples/src/main/java/org/apache/spark/examples/sql/JavaSparkSQLExample.java"中找到。

与RDD交互操作

Spark SQL支持两种不同的方法将现有RDD转换为Dataset。第一种方法使用反射来推断包含特定类型对象的RDD的模式。这种基于反射的方法使代码更简洁,在编写Spark应用程序时已经知道模式的情况下效果很好。

创建Dataset的第二种方法是通过编程接口,允许您构建模式,然后将其应用于现有RDD。虽然这种方法更繁琐,但它允许在运行时才知道列及其类型时构建Dataset。

使用反射推断模式

Spark SQL支持自动将JavaBeans的RDD转换为DataFrame。使用反射获取的BeanInfo定义表的模式。目前,Spark SQL不支持包含Map字段的JavaBeans。但支持嵌套的JavaBeans和List或Array字段。您可以通过创建一个实现Serializable接口并为其所有字段提供getter和setter的类来创建JavaBean。

import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.function.Function;
import org.apache.spark.api.java.function.MapFunction;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
import org.apache.spark.sql.Encoder;
import org.apache.spark.sql.Encoders;// 从文本文件创建Person对象的RDD
JavaRDD<Person> peopleRDD = spark.read().textFile("examples/src/main/resources/people.txt").javaRDD().map(line -> {String[] parts = line.split(",");Person person = new Person();person.setName(parts[0]);person.setAge(Integer.parseInt(parts[1].trim()));return person;});// 将模式应用于JavaBeans的RDD以获取DataFrame
Dataset<Row> peopleDF = spark.createDataFrame(peopleRDD, Person.class);
// 将DataFrame注册为临时视图
peopleDF.createOrReplaceTempView("people");// 可以使用spark提供的sql方法运行SQL语句
Dataset<Row> teenagersDF = spark.sql("SELECT name FROM people WHERE age BETWEEN 13 AND 19");// 可以通过字段索引访问结果中行的列
Encoder<String> stringEncoder = Encoders.STRING();
Dataset<String> teenagerNamesByIndexDF = teenagersDF.map((MapFunction<Row, String>) row -> "Name: " + row.getString(0),stringEncoder);
teenagerNamesByIndexDF.show();
// +------------+
// |       value|
// +------------+
// |Name: Justin|
// +------------+// 或通过字段名访问
Dataset<String> teenagerNamesByFieldDF = teenagersDF.map((MapFunction<Row, String>) row -> "Name: " + row.<String>getAs("name"),stringEncoder);
teenagerNamesByFieldDF.show();
// +------------+
// |       value|
// +------------+
// |Name: Justin|
// +------------+

完整示例代码可在Spark代码库的"examples/src/main/java/org/apache/spark/examples/sql/JavaSparkSQLExample.java"中找到。

以编程方式指定模式

当无法提前定义JavaBean类时(例如,记录的结构编码在字符串中,或者文本数据集将被解析并且字段将针对不同用户进行不同投影),可以通过三个步骤以编程方式创建Dataset:

  1. 从原始RDD创建Row的RDD;
  2. 创建与步骤1中创建的RDD中的Row结构匹配的StructType表示的模式;
  3. 通过SparkSession提供的createDataFrame方法将模式应用于Row的RDD。

例如:

import java.util.ArrayList;
import java.util.List;import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.function.Function;import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;import org.apache.spark.sql.types.DataTypes;
import org.apache.spark.sql.types.StructField;
import org.apache.spark.sql.types.StructType;// 创建RDD
JavaRDD<String> peopleRDD = spark.sparkContext().textFile("examples/src/main/resources/people.txt", 1).toJavaRDD();// 模式编码在字符串中
String schemaString = "name age";// 基于模式字符串生成模式
List<StructField> fields = new ArrayList<>();
for (String fieldName : schemaString.split(" ")) {StructField field = DataTypes.createStructField(fieldName, DataTypes.StringType, true);fields.add(field);
}
StructType schema = DataTypes.createStructType(fields);// 将RDD(people)的记录转换为Rows
JavaRDD<Row> rowRDD = peopleRDD.map((Function<String, Row>) record -> {String[] attributes = record.split(",");return RowFactory.create(attributes[0], attributes[1].trim());
});// 将模式应用于RDD
Dataset<Row> peopleDataFrame = spark.createDataFrame(rowRDD, schema);// 使用DataFrame创建临时视图
peopleDataFrame.createOrReplaceTempView("people");// 可以在使用DataFrame创建的临时视图上运行SQL
Dataset<Row> results = spark.sql("SELECT name FROM people");// SQL查询的结果是DataFrame,支持所有正常的RDD操作
// 可以通过字段索引或字段名访问结果中行的列
Dataset<String> namesDS = results.map((MapFunction<Row, String>) row -> "Name: " + row.getString(0),Encoders.STRING());
namesDS.show();
// +-------------+
// |        value|
// +-------------+
// |Name: Michael|
// |   Name: Andy|
// | Name: Justin|
// +-------------+

完整示例代码可在Spark代码库的"examples/src/main/java/org/apache/spark/examples/sql/JavaSparkSQLExample.java"中找到。

标量函数

标量函数是每行返回单个值的函数,与聚合函数(为行组返回一个值)相对。Spark SQL支持多种内置标量函数,也支持用户定义标量函数。

聚合函数

聚合函数是在行组上返回单个值的函数。内置聚合函数提供常见的聚合,如count()、count_distinct()、avg()、max()、min()等。用户不限于预定义的聚合函数,可以创建自己的聚合函数。有关用户定义聚合函数的更多详细信息,请参阅用户定义聚合函数的文档。

起点:SparkSession
创建DataFrame
无类型数据集操作(又称DataFrame操作)
以编程方式运行SQL查询
全局临时视图
创建数据集
与RDD交互操作
使用反射推断模式
以编程方式指定模式
标量函数
聚合函数

数据源

Spark SQL 支持通过 DataFrame 接口操作多种数据源。DataFrame 可以使用关系转换进行操作,也可用于创建临时视图。将 DataFrame 注册为临时视图后,即可对其数据运行 SQL 查询。本节介绍使用 Spark 数据源进行数据加载和保存的通用方法,然后详细介绍内置数据源可用的特定选项。

通用加载/保存函数

Python 方式

在最简单的形式中,默认数据源(parquet,除非通过spark.sql.sources.default另行配置)将用于所有操作。

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

完整示例代码可在Spark代码库的"examples/src/main/python/sql/datasource.py"中找到。

手动指定选项

您也可以手动指定要使用的数据源以及希望传递给数据源的任何额外选项。数据源由其完全限定名称指定(例如org.apache.spark.sql.parquet),但对于内置数据源,您也可以使用其短名称(jsonparquetjdbcorclibsvmcsvtext)。使用此语法,从任何数据源类型加载的DataFrame都可以转换为其他类型。

请参阅API文档了解内置数据源的可用选项,例如org.apache.spark.sql.DataFrameReaderorg.apache.spark.sql.DataFrameWriter。那里记录的选项也应适用于非Scala Spark API(如PySpark)。对于其他格式,请参阅特定格式的API文档。

要加载JSON文件,您可以使用:

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

完整示例代码可在Spark代码库的"examples/src/main/python/sql/datasource.py"中找到。

要加载CSV文件,您可以使用:

df = spark.read.load("examples/src/main/resources/people.csv",format="csv", sep=";", inferSchema="true", header="true")

完整示例代码可在Spark代码库的"examples/src/main/python/sql/datasource.py"中找到。

额外选项在写入操作期间也会使用。例如,您可以控制ORC数据源的布隆过滤器和字典编码。以下ORC示例将为favorite_color创建布隆过滤器并仅使用字典编码。对于Parquet,也存在parquet.bloom.filter.enabledparquet.enable.dictionary。要了解有关额外ORC/Parquet选项的更多详细信息,请访问官方Apache ORC / Parquet网站。

ORC数据源:

df = spark.read.orc("examples/src/main/resources/users.orc")
(df.write.format("orc").option("orc.bloom.filter.columns", "favorite_color").option("orc.dictionary.key.threshold", "1.0").option("orc.column.encoding.direct", "name").save("users_with_options.orc"))

完整示例代码可在Spark代码库的"examples/src/main/python/sql/datasource.py"中找到。

Parquet数据源:

df = spark.read.parquet("examples/src/main/resources/users.parquet")
(df.write.format("parquet").option("parquet.bloom.filter.enabled#favorite_color", "true").option("parquet.bloom.filter.expected.ndv#favorite_color", "1000000").option("parquet.enable.dictionary", "true").option("parquet.page.write-checksum.enabled", "false").save("users_with_options.parquet"))

完整示例代码可在Spark代码库的"examples/src/main/python/sql/datasource.py"中找到。

直接在文件上运行 SQL

除了使用read API将文件加载到DataFrame中进行查询外,您还可以直接使用SQL查询该文件。

df = spark.sql("SELECT * FROM parquet.`examples/src/main/resources/users.parquet`")

完整示例代码可在Spark代码库的"examples/src/main/python/sql/datasource.py"中找到。

保存模式

保存操作可以选择性地接受一个SaveMode,该模式指定如何处理现有数据(如果存在)。重要的是要认识到这些保存模式不使用任何锁定并且不是原子性的。此外,当执行Overwrite时,数据将在写出新数据之前被删除。

Scala/Java任何语言含义
SaveMode.ErrorIfExists (默认)“error” 或 “errorifexists” (默认)当将DataFrame保存到数据源时,如果数据已存在,预期会抛出异常。
SaveMode.Append“append”当将DataFrame保存到数据源时,如果数据/表已存在,DataFrame的内容预期会附加到现有数据中。
SaveMode.Overwrite“overwrite”覆盖模式意味着当将DataFrame保存到数据源时,如果数据/表已存在,现有数据预期会被DataFrame的内容覆盖。
SaveMode.Ignore“ignore”忽略模式意味着当将DataFrame保存到数据源时,如果数据已存在,保存操作预期不会保存DataFrame的内容,也不会更改现有数据。这类似于SQL中的CREATE TABLE IF NOT EXISTS
保存到持久化表

DataFrame也可以使用saveAsTable命令保存为Hive元存储中的持久表。请注意,使用此功能不需要现有的Hive部署。Spark将为您创建一个默认的本地Hive元存储(使用Derby)。与createOrReplaceTempView命令不同,saveAsTable将物化DataFrame的内容并在Hive元存储中创建指向数据的指针。只要您保持与同一元存储的连接,持久表即使在Spark程序重新启动后仍然存在。可以通过在SparkSession上调用table方法并指定表名来创建持久表的DataFrame。

对于基于文件的数据源,例如text、parquet、json等,您可以通过path选项指定自定义表路径,例如df.write.option("path", "/some/path").saveAsTable("t")。当表被删除时,自定义表路径不会被移除,表数据仍然存在。如果未指定自定义表路径,Spark会将数据写入仓库目录下的默认表路径。当表被删除时,默认表路径也会被移除。

从Spark 2.1开始,持久数据源表在Hive元存储中存储了每个分区的元数据。这带来了几个好处:

  • 由于元存储可以仅返回查询所需的分区,不再需要在首次查询表时发现所有分区。
  • 现在可以对使用数据源API创建的表使用Hive DDL,如ALTER TABLE PARTITION ... SET LOCATION

请注意,在创建外部数据源表(具有path选项的表)时,默认情况下不会收集分区信息。要同步元存储中的分区信息,可以调用MSCK REPAIR TABLE

分桶、排序和分区

对于基于文件的数据源,还可以对输出进行分桶、排序或分区。分桶和排序仅适用于持久表:

df.write.bucketBy(42, "name").sortBy("age").saveAsTable("people_bucketed")

完整示例代码可在Spark代码库的"examples/src/main/python/sql/datasource.py"中找到。

而在使用Dataset API时,分区可以与savesaveAsTable一起使用。

df.write.partitionBy("favorite_color").format("parquet").save("namesPartByColor.parquet")

完整示例代码可在Spark代码库的"examples/src/main/python/sql/datasource.py"中找到。

可以对单个表同时使用分区和分桶:

df = spark.read.parquet("examples/src/main/resources/users.parquet")
(df.write.partitionBy("favorite_color").bucketBy(42, "name").saveAsTable("users_partitioned_bucketed"))

完整示例代码可在Spark代码库的"examples/src/main/python/sql/datasource.py"中找到。

partitionBy创建如分区发现部分中所述的目录结构。因此,它对高基数列的适用性有限。相比之下,bucketBy将数据分布在固定数量的桶中,并且可以在唯一值数量无界时使用。

Scala 方式

在最简单的形式中,默认数据源(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")

完整示例代码可在Spark代码库的"examples/src/main/scala/org/apache/spark/examples/sql/SQLDataSourceExample.scala"中找到。

手动指定选项

您也可以手动指定要使用的数据源以及希望传递给数据源的任何额外选项。数据源由其完全限定名称指定(例如org.apache.spark.sql.parquet),但对于内置数据源,您也可以使用其短名称(jsonparquetjdbcorclibsvmcsvtext)。使用此语法,从任何数据源类型加载的DataFrame都可以转换为其他类型。

请参阅API文档了解内置数据源的可用选项,例如org.apache.spark.sql.DataFrameReaderorg.apache.spark.sql.DataFrameWriter。那里记录的选项也应适用于非Scala Spark API(如PySpark)。对于其他格式,请参阅特定格式的API文档。

要加载JSON文件,您可以使用:

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

完整示例代码可在Spark代码库的"examples/src/main/scala/org/apache/spark/examples/sql/SQLDataSourceExample.scala"中找到。

要加载CSV文件,您可以使用:

val peopleDFCsv = spark.read.format("csv").option("sep", ";").option("inferSchema", "true").option("header", "true").load("examples/src/main/resources/people.csv")

完整示例代码可在Spark代码库的"examples/src/main/scala/org/apache/spark/examples/sql/SQLDataSourceExample.scala"中找到。

额外选项在写入操作期间也会使用。例如,您可以控制ORC数据源的布隆过滤器和字典编码。以下ORC示例将为favorite_color创建布隆过滤器并仅使用字典编码。对于Parquet,也存在parquet.bloom.filter.enabledparquet.enable.dictionary。要了解有关额外ORC/Parquet选项的更多详细信息,请访问官方Apache ORC / Parquet网站。

ORC数据源:

usersDF.write.format("orc").option("orc.bloom.filter.columns", "favorite_color").option("orc.dictionary.key.threshold", "1.0").option("orc.column.encoding.direct", "name").save("users_with_options.orc")

完整示例代码可在Spark代码库的"examples/src/main/scala/org/apache/spark/examples/sql/SQLDataSourceExample.scala"中找到。

Parquet数据源:

usersDF.write.format("parquet").option("parquet.bloom.filter.enabled#favorite_color", "true").option("parquet.bloom.filter.expected.ndv#favorite_color", "1000000").option("parquet.enable.dictionary", "true").option("parquet.page.write-checksum.enabled", "false").save("users_with_options.parquet")

完整示例代码可在Spark代码库的"examples/src/main/scala/org/apache/spark/examples/sql/SQLDataSourceExample.scala"中找到。

直接在文件上运行SQL

除了使用read API将文件加载到DataFrame中进行查询外,您还可以直接使用SQL查询该文件。

val sqlDF = spark.sql("SELECT * FROM parquet.`examples/src/main/resources/users.parquet`")

完整示例代码可在Spark代码库的"examples/src/main/scala/org/apache/spark/examples/sql/SQLDataSourceExample.scala"中找到。

保存模式

保存操作可以选择性地接受一个SaveMode,该模式指定如何处理现有数据(如果存在)。重要的是要认识到这些保存模式不使用任何锁定并且不是原子性的。此外,当执行Overwrite时,数据将在写出新数据之前被删除。

Scala/Java任何语言含义
SaveMode.ErrorIfExists (默认)“error” 或 “errorifexists” (默认)当将DataFrame保存到数据源时,如果数据已存在,预期会抛出异常。
SaveMode.Append“append”当将DataFrame保存到数据源时,如果数据/表已存在,DataFrame的内容预期会附加到现有数据中。
SaveMode.Overwrite“overwrite”覆盖模式意味着当将DataFrame保存到数据源时,如果数据/表已存在,现有数据预期会被DataFrame的内容覆盖。
SaveMode.Ignore“ignore”忽略模式意味着当将DataFrame保存到数据源时,如果数据已存在,保存操作预期不会保存DataFrame的内容,也不会更改现有数据。这类似于SQL中的CREATE TABLE IF NOT EXISTS
保存到持久表

DataFrame也可以使用saveAsTable命令保存为Hive元存储中的持久表。请注意,使用此功能不需要现有的Hive部署。Spark将为您创建一个默认的本地Hive元存储(使用Derby)。与createOrReplaceTempView命令不同,saveAsTable将物化DataFrame的内容并在Hive元存储中创建指向数据的指针。只要您保持与同一元存储的连接,持久表即使在Spark程序重新启动后仍然存在。可以通过在SparkSession上调用table方法并指定表名来创建持久表的DataFrame。

对于基于文件的数据源,例如text、parquet、json等,您可以通过path选项指定自定义表路径,例如df.write.option("path", "/some/path").saveAsTable("t")。当表被删除时,自定义表路径不会被移除,表数据仍然存在。如果未指定自定义表路径,Spark会将数据写入仓库目录下的默认表路径。当表被删除时,默认表路径也会被移除。

从Spark 2.1开始,持久数据源表在Hive元存储中存储了每个分区的元数据。这带来了几个好处:

  • 由于元存储可以仅返回查询所需的分区,不再需要在首次查询表时发现所有分区。
  • 现在可以对使用数据源API创建的表使用Hive DDL,如ALTER TABLE PARTITION ... SET LOCATION

请注意,在创建外部数据源表(具有path选项的表)时,默认情况下不会收集分区信息。要同步元存储中的分区信息,可以调用MSCK REPAIR TABLE

分桶、排序和分区

对于基于文件的数据源,还可以对输出进行分桶、排序或分区。分桶和排序仅适用于持久表:

peopleDF.write.bucketBy(42, "name").sortBy("age").saveAsTable("people_bucketed")

完整示例代码可在Spark代码库的"examples/src/main/scala/org/apache/spark/examples/sql/SQLDataSourceExample.scala"中找到。

而在使用Dataset API时,分区可以与savesaveAsTable一起使用。

usersDF.write.partitionBy("favorite_color").format("parquet").save("namesPartByColor.parquet")

完整示例代码可在Spark代码库的"examples/src/main/scala/org/apache/spark/examples/sql/SQLDataSourceExample.scala"中找到。

可以对单个表同时使用分区和分桶:

usersDF.write.partitionBy("favorite_color").bucketBy(42, "name").saveAsTable("users_partitioned_bucketed")

完整示例代码可在Spark代码库的"examples/src/main/scala/org/apache/spark/examples/sql/SQLDataSourceExample.scala"中找到。

partitionBy创建如分区发现部分中所述的目录结构。因此,它对高基数列的适用性有限。相比之下,bucketBy将数据分布在固定数量的桶中,并且可以在唯一值数量无界时使用。

Java 方式

在最简单的形式中,默认数据源(parquet,除非通过spark.sql.sources.default另行配置)将用于所有操作。

Dataset<Row> usersDF = spark.read().load("examples/src/main/resources/users.parquet");
usersDF.select("name", "favorite_color").write().save("namesAndFavColors.parquet");

完整示例代码可在Spark代码库的"examples/src/main/java/org/apache/spark/examples/sql/JavaSQLDataSourceExample.java"中找到。

手动指定选项

您也可以手动指定要使用的数据源以及希望传递给数据源的任何额外选项。数据源由其完全限定名称指定(例如org.apache.spark.sql.parquet),但对于内置数据源,您也可以使用其短名称(jsonparquetjdbcorclibsvmcsvtext)。使用此语法,从任何数据源类型加载的DataFrame都可以转换为其他类型。

请参阅API文档了解内置数据源的可用选项,例如org.apache.spark.sql.DataFrameReaderorg.apache.spark.sql.DataFrameWriter。那里记录的选项也应适用于非Scala Spark API(如PySpark)。对于其他格式,请参阅特定格式的API文档。

要加载JSON文件,您可以使用:

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

完整示例代码可在Spark代码库的"examples/src/main/java/org/apache/spark/examples/sql/JavaSQLDataSourceExample.java"中找到。

要加载CSV文件,您可以使用:

Dataset<Row> peopleDFCsv = spark.read().format("csv").option("sep", ";").option("inferSchema", "true").option("header", "true").load("examples/src/main/resources/people.csv");

完整示例代码可在Spark代码库的"examples/src/main/java/org/apache/spark/examples/sql/JavaSQLDataSourceExample.java"中找到。

额外选项在写入操作期间也会使用。例如,您可以控制ORC数据源的布隆过滤器和字典编码。以下ORC示例将为favorite_color创建布隆过滤器并仅使用字典编码。对于Parquet,也存在parquet.bloom.filter.enabledparquet.enable.dictionary。要了解有关额外ORC/Parquet选项的更多详细信息,请访问官方Apache ORC / Parquet网站。

ORC数据源:

usersDF.write().format("orc").option("orc.bloom.filter.columns", "favorite_color").option("orc.dictionary.key.threshold", "1.0").option("orc.column.encoding.direct", "name").save("users_with_options.orc");

完整示例代码可在Spark代码库的"examples/src/main/java/org/apache/spark/examples/sql/JavaSQLDataSourceExample.java"中找到。

Parquet数据源:

usersDF.write().format("parquet").option("parquet.bloom.filter.enabled#favorite_color", "true").option("parquet.bloom.filter.expected.ndv#favorite_color", "1000000").option("parquet.enable.dictionary", "true").option("parquet.page.write-checksum.enabled", "false").save("users_with_options.parquet");

完整示例代码可在Spark代码库的"examples/src/main/java/org/apache/spark/examples/sql/JavaSQLDataSourceExample.java"中找到。

直接在文件上运行SQL

除了使用read API将文件加载到DataFrame中进行查询外,您还可以直接使用SQL查询该文件。

Dataset<Row> sqlDF =spark.sql("SELECT * FROM parquet.`examples/src/main/resources/users.parquet`");

完整示例代码可在Spark代码库的"examples/src/main/java/org/apache/spark/examples/sql/JavaSQLDataSourceExample.java"中找到。

保存模式

保存操作可以选择性地接受一个SaveMode,该模式指定如何处理现有数据(如果存在)。重要的是要认识到这些保存模式不使用任何锁定并且不是原子性的。此外,当执行Overwrite时,数据将在写出新数据之前被删除。

Scala/Java任何语言含义
SaveMode.ErrorIfExists (默认)“error” 或 “errorifexists” (默认)当将DataFrame保存到数据源时,如果数据已存在,预期会抛出异常。
SaveMode.Append“append”当将DataFrame保存到数据源时,如果数据/表已存在,DataFrame的内容预期会附加到现有数据中。
SaveMode.Overwrite“overwrite”覆盖模式意味着当将DataFrame保存到数据源时,如果数据/表已存在,现有数据预期会被DataFrame的内容覆盖。
SaveMode.Ignore“ignore”忽略模式意味着当将DataFrame保存到数据源时,如果数据已存在,保存操作预期不会保存DataFrame的内容,也不会更改现有数据。这类似于SQL中的CREATE TABLE IF NOT EXISTS
保存到持久表

DataFrame也可以使用saveAsTable命令保存为Hive元存储中的持久表。请注意,使用此功能不需要现有的Hive部署。Spark将为您创建一个默认的本地Hive元存储(使用Derby)。与createOrReplaceTempView命令不同,saveAsTable将物化DataFrame的内容并在Hive元存储中创建指向数据的指针。只要您保持与同一元存储的连接,持久表即使在Spark程序重新启动后仍然存在。可以通过在SparkSession上调用table方法并指定表名来创建持久表的DataFrame。

对于基于文件的数据源,例如text、parquet、json等,您可以通过path选项指定自定义表路径,例如df.write.option("path", "/some/path").saveAsTable("t")。当表被删除时,自定义表路径不会被移除,表数据仍然存在。如果未指定自定义表路径,Spark会将数据写入仓库目录下的默认表路径。当表被删除时,默认表路径也会被移除。

从Spark 2.1开始,持久数据源表在Hive元存储中存储了每个分区的元数据。这带来了几个好处:

  • 由于元存储可以仅返回查询所需的分区,不再需要在首次查询表时发现所有分区。
  • 现在可以对使用数据源API创建的表使用Hive DDL,如ALTER TABLE PARTITION ... SET LOCATION

请注意,在创建外部数据源表(具有path选项的表)时,默认情况下不会收集分区信息。要同步元存储中的分区信息,可以调用MSCK REPAIR TABLE

分桶、排序和分区

对于基于文件的数据源,还可以对输出进行分桶、排序或分区。分桶和排序仅适用于持久表:

peopleDF.write().bucketBy(42, "name").sortBy("age").saveAsTable("people_bucketed");

完整示例代码可在Spark代码库的"examples/src/main/java/org/apache/spark/examples/sql/JavaSQLDataSourceExample.java"中找到。

而在使用Dataset API时,分区可以与savesaveAsTable一起使用。

usersDF.write().partitionBy("favorite_color").format("parquet").save("namesPartByColor.parquet");

完整示例代码可在Spark代码库的"examples/src/main/java/org/apache/spark/examples/sql/JavaSQLDataSourceExample.java"中找到。

可以对单个表同时使用分区和分桶:

usersDF.write().partitionBy("favorite_color").bucketBy(42, "name").saveAsTable("users_partitioned_bucketed");

完整示例代码可在Spark代码库的"examples/src/main/java/org/apache/spark/examples/sql/JavaSQLDataSourceExample.java"中找到。

partitionBy创建如分区发现部分中所述的目录结构。因此,它对高基数列的适用性有限。相比之下,bucketBy将数据分布在固定数量的桶中,并且可以在唯一值数量无界时使用。

通用文件源选项

Python 方式

这些通用选项/配置仅在使用基于文件的数据源时有效:parquet、orc、avro、json、csv、text。

请注意,以下示例中使用的目录层次结构为:

dir1/├── dir2/│    └── file2.parquet (schema: <file: string>, content: "file2.parquet")└── file1.parquet (schema: <file, string>, content: "file1.parquet")└── file3.json (schema: <file, string>, content: "{'file':'corrupt.json'}")
忽略损坏文件

Spark允许您使用配置spark.sql.files.ignoreCorruptFiles或数据源选项ignoreCorruptFiles在从文件读取数据时忽略损坏的文件。当设置为true时,Spark作业在遇到损坏的文件时会继续运行,并且已读取的内容仍将返回。

要在读取数据文件时忽略损坏文件,您可以使用:

# 通过数据源选项启用忽略损坏文件
# dir1/file3.json 从parquet的角度看是损坏的
test_corrupt_df0 = spark.read.option("ignoreCorruptFiles", "true")\.parquet("examples/src/main/resources/dir1/","examples/src/main/resources/dir1/dir2/")
test_corrupt_df0.show()
# +-------------+
# |         file|
# +-------------+
# |file1.parquet|
# |file2.parquet|
# +-------------+# 通过配置启用忽略损坏文件
spark.sql("set spark.sql.files.ignoreCorruptFiles=true")
# dir1/file3.json 从parquet的角度看是损坏的
test_corrupt_df1 = spark.read.parquet("examples/src/main/resources/dir1/","examples/src/main/resources/dir1/dir2/")
test_corrupt_df1.show()
# +-------------+
# |         file|
# +-------------+
# |file1.parquet|
# |file2.parquet|
# +-------------+

完整示例代码可在Spark代码库的"examples/src/main/python/sql/datasource.py"中找到。

忽略缺失文件

Spark允许您使用配置spark.sql.files.ignoreMissingFiles或数据源选项ignoreMissingFiles在从文件读取数据时忽略缺失的文件。这里的缺失文件指的是在构建DataFrame之后目录下被删除的文件。当设置为true时,Spark作业在遇到缺失文件时会继续运行,并且已读取的内容仍将返回。

路径通配符过滤器

pathGlobFilter用于仅包含文件名匹配模式的文件。语法遵循org.apache.hadoop.fs.GlobFilter。它不会改变分区发现的行为。

要在保持分区发现行为的同时加载路径匹配给定通配符模式的文件,您可以使用:

df = spark.read.load("examples/src/main/resources/dir1",format="parquet", pathGlobFilter="*.parquet")
df.show()
# +-------------+
# |         file|
# +-------------+
# |file1.parquet|
# +-------------+

完整示例代码可在Spark代码库的"examples/src/main/python/sql/datasource.py"中找到。

递归文件查找

recursiveFileLookup用于递归加载文件,并且会禁用分区推断。其默认值为false。如果数据源在recursiveFileLookup为true时显式指定了partitionSpec,将抛出异常。

要递归加载所有文件,您可以使用:

recursive_loaded_df = spark.read.format("parquet")\.option("recursiveFileLookup", "true")\.load("examples/src/main/resources/dir1")
recursive_loaded_df.show()
# +-------------+
# |         file|
# +-------------+
# |file1.parquet|
# |file2.parquet|
# +-------------+

完整示例代码可在Spark代码库的"examples/src/main/python/sql/datasource.py"中找到。

修改时间路径过滤器

modifiedBeforemodifiedAfter是可以单独或一起应用的选项,以便在Spark批处理查询期间对可能加载的文件实现更精细的控制。(请注意,结构化流文件源不支持这些选项。)

  • modifiedBefore:一个可选的时间戳,仅包含修改时间发生在指定时间之前的文件。提供的时间戳必须采用以下格式:YYYY-MM-DDTHH:mm:ss(例如2020-06-01T13:00:00)
  • modifiedAfter:一个可选的时间戳,仅包含修改时间发生在指定时间之后的文件。提供的时间戳必须采用以下格式:YYYY-MM-DDTHH:mm:ss(例如2020-06-01T13:00:00)

当未提供时区选项时,时间戳将根据Spark会话时区(spark.sql.session.timeZone)进行解释。

要加载路径匹配给定修改时间范围的文件,您可以使用:

# 仅加载在2050年7月1日08:30:00之前修改的文件
df = spark.read.load("examples/src/main/resources/dir1",format="parquet", modifiedBefore="2050-07-01T08:30:00")
df.show()
# +-------------+
# |         file|
# +-------------+
# |file1.parquet|
# +-------------+# 仅加载在2050年6月1日08:30:00之后修改的文件
df = spark.read.load("examples/src/main/resources/dir1",format="parquet", modifiedAfter="2050-06-01T08:30:00")
df.show()
# +-------------+
# |         file|
# +-------------+
# +-------------+

完整示例代码可在Spark代码库的"examples/src/main/python/sql/datasource.py"中找到。

Scala 方式

这些通用选项/配置仅在使用基于文件的数据源时有效:parquet、orc、avro、json、csv、text。

请注意,以下示例中使用的目录层次结构为:

dir1/├── dir2/│    └── file2.parquet (schema: <file: string>, content: "file2.parquet")└── file1.parquet (schema: <file, string>, content: "file1.parquet")└── file3.json (schema: <file, string>, content: "{'file':'corrupt.json'}")
忽略损坏文件

Spark允许您使用配置spark.sql.files.ignoreCorruptFiles或数据源选项ignoreCorruptFiles在从文件读取数据时忽略损坏的文件。当设置为true时,Spark作业在遇到损坏的文件时会继续运行,并且已读取的内容仍将返回。

要在读取数据文件时忽略损坏文件,您可以使用:

// 通过数据源选项启用忽略损坏文件
// dir1/file3.json 从parquet的角度看是损坏的
val testCorruptDF0 = spark.read.option("ignoreCorruptFiles", "true").parquet("examples/src/main/resources/dir1/","examples/src/main/resources/dir1/dir2/")
testCorruptDF0.show()
// +-------------+
// |         file|
// +-------------+
// |file1.parquet|
// |file2.parquet|
// +-------------+// 通过配置启用忽略损坏文件
spark.sql("set spark.sql.files.ignoreCorruptFiles=true")
// dir1/file3.json 从parquet的角度看是损坏的
val testCorruptDF1 = spark.read.parquet("examples/src/main/resources/dir1/","examples/src/main/resources/dir1/dir2/")
testCorruptDF1.show()
// +-------------+
// |         file|
// +-------------+
// |file1.parquet|
// |file2.parquet|
// +-------------+

完整示例代码可在Spark代码库的"examples/src/main/scala/org/apache/spark/examples/sql/SQLDataSourceExample.scala"中找到。

忽略缺失文件

Spark允许您使用配置spark.sql.files.ignoreMissingFiles或数据源选项ignoreMissingFiles在从文件读取数据时忽略缺失的文件。这里的缺失文件指的是在构建DataFrame之后目录下被删除的文件。当设置为true时,Spark作业在遇到缺失文件时会继续运行,并且已读取的内容仍将返回。

路径通配符过滤器

pathGlobFilter用于仅包含文件名匹配模式的文件。语法遵循org.apache.hadoop.fs.GlobFilter。它不会改变分区发现的行为。

要在保持分区发现行为的同时加载路径匹配给定通配符模式的文件,您可以使用:

val testGlobFilterDF = spark.read.format("parquet").option("pathGlobFilter", "*.parquet") // json文件应该被过滤掉.load("examples/src/main/resources/dir1")
testGlobFilterDF.show()
// +-------------+
// |         file|
// +-------------+
// |file1.parquet|
// +-------------+

完整示例代码可在Spark代码库的"examples/src/main/scala/org/apache/spark/examples/sql/SQLDataSourceExample.scala"中找到。

递归文件查找

recursiveFileLookup用于递归加载文件,并且会禁用分区推断。其默认值为false。如果数据源在recursiveFileLookup为true时显式指定了partitionSpec,将抛出异常。

要递归加载所有文件,您可以使用:

val recursiveLoadedDF = spark.read.format("parquet").option("recursiveFileLookup", "true").load("examples/src/main/resources/dir1")
recursiveLoadedDF.show()
// +-------------+
// |         file|
// +-------------+
// |file1.parquet|
// |file2.parquet|
// +-------------+

完整示例代码可在Spark代码库的"examples/src/main/scala/org/apache/spark/examples/sql/SQLDataSourceExample.scala"中找到。

修改时间路径过滤器

modifiedBeforemodifiedAfter是可以单独或一起应用的选项,以便在Spark批处理查询期间对可能加载的文件实现更精细的控制。(请注意,结构化流文件源不支持这些选项。)

  • modifiedBefore:一个可选的时间戳,仅包含修改时间发生在指定时间之前的文件。提供的时间戳必须采用以下格式:YYYY-MM-DDTHH:mm:ss(例如2020-06-01T13:00:00)
  • modifiedAfter:一个可选的时间戳,仅包含修改时间发生在指定时间之后的文件。提供的时间戳必须采用以下格式:YYYY-MM-DDTHH:mm:ss(例如2020-06-01T13:00:00)

当未提供时区选项时,时间戳将根据Spark会话时区(spark.sql.session.timeZone)进行解释。

要加载路径匹配给定修改时间范围的文件,您可以使用:

val beforeFilterDF = spark.read.format("parquet")// 允许在2020年7月1日05:30之前修改的文件.option("modifiedBefore", "2020-07-01T05:30:00").load("examples/src/main/resources/dir1");
beforeFilterDF.show();
// +-------------+
// |         file|
// +-------------+
// |file1.parquet|
// +-------------+val afterFilterDF = spark.read.format("parquet")// 允许在2020年6月1日05:30之后修改的文件.option("modifiedAfter", "2020-06-01T05:30:00").load("examples/src/main/resources/dir1");
afterFilterDF.show();
// +-------------+
// |         file|
// +-------------+
// +-------------+

完整示例代码可在Spark代码库的"examples/src/main/scala/org/apache/spark/examples/sql/SQLDataSourceExample.scala"中找到。

Java 方式

这些通用选项/配置仅在使用基于文件的数据源时有效:parquet、orc、avro、json、csv、text。

请注意,以下示例中使用的目录层次结构为:

dir1/├── dir2/│    └── file2.parquet (schema: <file: string>, content: "file2.parquet")└── file1.parquet (schema: <file, string>, content: "file1.parquet")└── file3.json (schema: <file, string>, content: "{'file':'corrupt.json'}")
忽略损坏文件

Spark允许您使用配置spark.sql.files.ignoreCorruptFiles或数据源选项ignoreCorruptFiles在从文件读取数据时忽略损坏的文件。当设置为true时,Spark作业在遇到损坏的文件时会继续运行,并且已读取的内容仍将返回。

要在读取数据文件时忽略损坏文件,您可以使用:

// 通过数据源选项启用忽略损坏文件
// dir1/file3.json 从parquet的角度看是损坏的
Dataset<Row> testCorruptDF0 = spark.read().option("ignoreCorruptFiles", "true").parquet("examples/src/main/resources/dir1/","examples/src/main/resources/dir1/dir2/");
testCorruptDF0.show();
// +-------------+
// |         file|
// +-------------+
// |file1.parquet|
// |file2.parquet|
// +-------------+// 通过配置启用忽略损坏文件
spark.sql("set spark.sql.files.ignoreCorruptFiles=true");
// dir1/file3.json 从parquet的角度看是损坏的
Dataset<Row> testCorruptDF1 = spark.read().parquet("examples/src/main/resources/dir1/","examples/src/main/resources/dir1/dir2/");
testCorruptDF1.show();
// +-------------+
// |         file|
// +-------------+
// |file1.parquet|
// |file2.parquet|
// +-------------+

完整示例代码可在Spark代码库的"examples/src/main/java/org/apache/spark/examples/sql/JavaSQLDataSourceExample.java"中找到。

忽略缺失文件

Spark允许您使用配置spark.sql.files.ignoreMissingFiles或数据源选项ignoreMissingFiles在从文件读取数据时忽略缺失的文件。这里的缺失文件指的是在构建DataFrame之后目录下被删除的文件。当设置为true时,Spark作业在遇到缺失文件时会继续运行,并且已读取的内容仍将返回。

路径通配符过滤器

pathGlobFilter用于仅包含文件名匹配模式的文件。语法遵循org.apache.hadoop.fs.GlobFilter。它不会改变分区发现的行为。

要在保持分区发现行为的同时加载路径匹配给定通配符模式的文件,您可以使用:

Dataset<Row> testGlobFilterDF = spark.read().format("parquet").option("pathGlobFilter", "*.parquet") // json文件应该被过滤掉.load("examples/src/main/resources/dir1");
testGlobFilterDF.show();
// +-------------+
// |         file|
// +-------------+
// |file1.parquet|
// +-------------+

完整示例代码可在Spark代码库的"examples/src/main/java/org/apache/spark/examples/sql/JavaSQLDataSourceExample.java"中找到。

递归文件查找

recursiveFileLookup用于递归加载文件,并且会禁用分区推断。其默认值为false。如果数据源在recursiveFileLookup为true时显式指定了partitionSpec,将抛出异常。

要递归加载所有文件,您可以使用:

Dataset<Row> recursiveLoadedDF = spark.read().format("parquet").option("recursiveFileLookup", "true").load("examples/src/main/resources/dir1");
recursiveLoadedDF.show();
// +-------------+
// |         file|
// +-------------+
// |file1.parquet|
// |file2.parquet|
// +-------------+

完整示例代码可在Spark代码库的"examples/src/main/java/org/apache/spark/examples/sql/JavaSQLDataSourceExample.java"中找到。

修改时间路径过滤器

modifiedBeforemodifiedAfter是可以单独或一起应用的选项,以便在Spark批处理查询期间对可能加载的文件实现更精细的控制。(请注意,结构化流文件源不支持这些选项。)

  • modifiedBefore:一个可选的时间戳,仅包含修改时间发生在指定时间之前的文件。提供的时间戳必须采用以下格式:YYYY-MM-DDTHH:mm:ss(例如2020-06-01T13:00:00)
  • modifiedAfter:一个可选的时间戳,仅包含修改时间发生在指定时间之后的文件。提供的时间戳必须采用以下格式:YYYY-MM-DDTHH:mm:ss(例如2020-06-01T13:00:00)

当未提供时区选项时,时间戳将根据Spark会话时区(spark.sql.session.timeZone)进行解释。

要加载路径匹配给定修改时间范围的文件,您可以使用:

Dataset<Row> beforeFilterDF = spark.read().format("parquet")// 仅加载在2020年7月1日05:30之前修改的文件.option("modifiedBefore", "2020-07-01T05:30:00")// 仅加载在2020年6月1日05:30之后修改的文件.option("modifiedAfter", "2020-06-01T05:30:00")// 相对于CST时区解释上述两个时间.option("timeZone", "CST").load("examples/src/main/resources/dir1");
beforeFilterDF.show();
// +-------------+
// |         file|
// +-------------+
// |file1.parquet|
// +-------------+

完整示例代码可在Spark代码库的"examples/src/main/java/org/apache/spark/examples/sql/JavaSQLDataSourceExample.java"中找到。

Parquet 文件

Parquet是一种列式格式,受到许多其他数据处理系统的支持。Spark SQL提供了对读写Parquet文件的支持,能够自动保留原始数据的模式。在读取Parquet文件时,出于兼容性原因,所有列都会自动转换为可空。

Python 文件
以编程方式加载数据

使用上述示例中的数据:

peopleDF = spark.read.json("examples/src/main/resources/people.json")# DataFrame可以保存为Parquet文件,保留模式信息
peopleDF.write.parquet("people.parquet")# 读取上面创建的Parquet文件
# Parquet文件是自描述的,因此模式被保留
# 加载parquet文件的结果也是一个DataFrame
parquetFile = spark.read.parquet("people.parquet")# Parquet文件也可以用于创建临时视图,然后在SQL语句中使用
parquetFile.createOrReplaceTempView("parquetFile")
teenagers = spark.sql("SELECT name FROM parquetFile WHERE age >= 13 AND age <= 19")
teenagers.show()
# +------+
# |  name|
# +------+
# |Justin|
# +------+

完整示例代码可在Spark代码库的"examples/src/main/python/sql/datasource.py"中找到。

分区发现

表分区是Hive等系统中常用的优化方法。在分区表中,数据通常存储在不同的目录中,分区列值编码在每个分区目录的路径中。所有内置文件源(包括Text/CSV/JSON/ORC/Parquet)都能够自动发现和推断分区信息。例如,我们可以使用以下目录结构将之前使用的人口数据存储到分区表中,其中两个额外的列gendercountry作为分区列:

path
└── to└── table├── gender=male│   ├── ...│   ││   ├── country=US│   │   └── data.parquet│   ├── country=CN│   │   └── data.parquet│   └── ...└── gender=female├── ...│├── country=US│   └── data.parquet├── country=CN│   └── data.parquet└── ...

通过将path/to/table传递给SparkSession.read.parquetSparkSession.read.load,Spark SQL将自动从路径中提取分区信息。现在返回的DataFrame的模式变为:

root
|-- name: string (nullable = true)
|-- age: long (nullable = true)
|-- gender: string (nullable = true)
|-- country: string (nullable = true)

请注意,分区列的数据类型是自动推断的。目前支持数值数据类型、日期、时间戳和字符串类型。有时用户可能不希望自动推断分区列的数据类型。对于这些用例,可以通过spark.sql.sources.partitionColumnTypeInference.enabled配置自动类型推断,该配置默认为true。当类型推断被禁用时,分区列将使用字符串类型。

从Spark 1.6.0开始,默认情况下分区发现只查找给定路径下的分区。对于上面的例子,如果用户将path/to/table/gender=male传递给SparkSession.read.parquetSparkSession.read.loadgender将不会被视作分区列。如果用户需要指定分区发现应该开始的基路径,可以在数据源选项中设置basePath。例如,当path/to/table/gender=male是数据路径且用户将basePath设置为path/to/table/时,gender将是一个分区列。

模式合并

与Protocol Buffer、Avro和Thrift类似,Parquet也支持模式演化。用户可以从一个简单的模式开始,然后根据需要逐渐向模式添加更多列。这样,用户最终可能会得到多个具有不同但相互兼容模式的Parquet文件。Parquet数据源现在能够自动检测这种情况并合并所有这些文件的模式。

由于模式合并是一个相对昂贵的操作,并且在大多数情况下不是必需的,我们从1.5.0开始默认关闭了它。您可以通过以下方式启用它:

  • 在读取Parquet文件时设置数据源选项mergeSchema为true(如下面的示例所示),或
  • 设置全局SQL选项spark.sql.parquet.mergeSchema为true。
from pyspark.sql import Row# spark来自上一个示例
# 创建一个简单的DataFrame,存储到分区目录
sc = spark.sparkContextsquaresDF = spark.createDataFrame(sc.parallelize(range(1, 6)).map(lambda i: Row(single=i, double=i ** 2)))
squaresDF.write.parquet("data/test_table/key=1")# 在新分区目录中创建另一个DataFrame,
# 添加新列并删除现有列
cubesDF = spark.createDataFrame(sc.parallelize(range(6, 11)).map(lambda i: Row(single=i, triple=i ** 3)))
cubesDF.write.parquet("data/test_table/key=2")# 读取分区表
mergedDF = spark.read.option("mergeSchema", "true").parquet("data/test_table")
mergedDF.printSchema()# 最终模式包含Parquet文件中的所有3列
# 以及出现在分区目录路径中的分区列
# root
#  |-- double: long (nullable = true)
#  |-- single: long (nullable = true)
#  |-- triple: long (nullable = true)
#  |-- key: integer (nullable = true)

完整示例代码可在Spark代码库的"examples/src/main/python/sql/datasource.py"中找到。

Hive元存储Parquet表转换

当从Hive元存储Parquet表读取数据并写入到非分区的Hive元存储Parquet表时,Spark SQL将尝试使用自己的Parquet支持而不是Hive SerDe以获得更好的性能。此行为由spark.sql.hive.convertMetastoreParquet配置控制,默认开启。

Hive/Parquet模式协调

从表模式处理的角度来看,Hive和Parquet有两个关键区别:

  1. Hive不区分大小写,而Parquet区分
  2. Hive认为所有列都可为空,而Parquet中的可空性很重要

由于这个原因,在将Hive元存储Parquet表转换为Spark SQL Parquet表时,我们必须协调Hive元存储模式和Parquet模式。协调规则是:

  • 在两个模式中具有相同名称的字段必须具有相同的数据类型,无论可空性如何。协调后的字段应具有Parquet端的数据类型,以便尊重可空性。
  • 协调后的模式恰好包含Hive元存储模式中定义的字段。
    • 任何仅在Parquet模式中出现的字段在协调后的模式中被丢弃。
    • 任何仅在Hive元存储模式中出现的字段在协调后的模式中作为可空字段添加。
元数据刷新

Spark SQL会缓存Parquet元数据以提高性能。当启用Hive元存储Parquet表转换时,这些转换表的元数据也会被缓存。如果这些表被Hive或其他外部工具更新,您需要手动刷新它们以确保元数据一致。

# spark是现有的SparkSession
spark.catalog.refreshTable("my_table")
列式加密

从Spark 3.2开始,支持使用Apache Parquet 1.12+的Parquet表的列式加密。

Parquet使用信封加密实践,其中文件部分使用"数据加密密钥"(DEK)加密,而DEK使用"主加密密钥"(MEK)加密。DEK由Parquet为每个加密文件/列随机生成。MEK在用户选择的密钥管理服务(KMS)中生成、存储和管理。Parquet Maven仓库有一个包含模拟KMS实现的jar,允许仅使用spark-shell运行列加密和解密,而无需部署KMS服务器(下载parquet-hadoop-tests.jar文件并将其放在Spark jars文件夹中):

# 设置hadoop配置属性,例如使用Spark作业的配置属性:
# --conf spark.hadoop.parquet.encryption.kms.client.class=\
#           "org.apache.parquet.crypto.keytools.mocks.InMemoryKMS"\
# --conf spark.hadoop.parquet.encryption.key.list=\
#           "keyA:AAECAwQFBgcICQoLDA0ODw== ,  keyB:AAECAAECAAECAAECAAECAA=="\
# --conf spark.hadoop.parquet.crypto.factory.class=\
#           "org.apache.parquet.crypto.keytools.PropertiesDrivenCryptoFactory"# 写入加密的dataframe文件
# 列"square"将使用主密钥"keyA"保护
# Parquet文件页脚将使用主密钥"keyB"保护
squaresDF.write\.option("parquet.encryption.column.keys" , "keyA:square")\.option("parquet.encryption.footer.key" , "keyB")\.parquet("/path/to/table.parquet.encrypted")# 读取加密的dataframe文件
df2 = spark.read.parquet("/path/to/table.parquet.encrypted")
KMS客户端

InMemoryKMS类仅用于说明和简单演示Parquet加密功能,不应在实际部署中使用。主加密密钥必须保存在用户组织中部署的生产级KMS系统中。推出带有Parquet加密的Spark需要为KMS服务器实现一个客户端类。Parquet提供了一个插件接口用于开发此类类:

public interface KmsClient {// 包装密钥 - 使用主密钥加密它public String wrapKey(byte[] keyBytes, String masterKeyIdentifier);// 使用主密钥解密(解包)密钥public byte[] unwrapKey(String wrappedKey, String masterKeyIdentifier);// 初始化参数的使用是可选的public void initialize(Configuration configuration, String kmsInstanceID,String kmsInstanceURL, String accessToken);
}

可以在parquet-mr仓库中找到此类类的开源KMS示例。生产KMS客户端应与组织的安全管理员合作设计,并由具有访问控制管理经验的开发人员构建。一旦创建了这样的类,就可以通过parquet.encryption.kms.client.class参数传递给应用程序,并由普通Spark用户利用,如上所示的加密dataframe写入/读取示例。

注意:默认情况下,Parquet实现了"双重信封加密"模式,最大限度地减少了Spark执行器与KMS服务器的交互。在此模式下,DEK使用"密钥加密密钥"(KEK,由Parquet随机生成)加密。KEK在KMS中使用MEK加密;结果和KEK本身缓存在Spark执行器内存中。对常规信封加密感兴趣的用户可以通过将parquet.encryption.double.wrapping参数设置为false来切换到它。有关Parquet加密参数的更多详细信息,请访问parquet-hadoop配置页面。

数据源选项

Parquet的数据源选项可以通过以下方式设置:

  • DataFrameReaderDataFrameWriterDataStreamReaderDataStreamWriter.option/.options方法
  • CREATE TABLE USING DATA_SOURCE中的OPTIONS子句
属性名称默认值含义作用范围
datetimeRebaseModespark.sql.parquet.datetimeRebaseModeInRead配置的值datetimeRebaseMode选项允许指定从儒略历到公历的DATE、TIMESTAMP_MILLIS、TIMESTAMP_MICROS逻辑类型值的重新基准模式。
当前支持的模式有:
EXCEPTION:在读取两个日历之间不明确的古老日期/时间戳时失败。
CORRECTED:加载日期/时间戳而不重新基准。
LEGACY:将古老日期/时间戳从儒略历重新基准到公历。
读取
int96RebaseModespark.sql.parquet.int96RebaseModeInRead配置的值int96RebaseMode选项允许指定从儒略历到公历的INT96时间戳的重新基准模式。
当前支持的模式有:
EXCEPTION:在读取两个日历之间不明确的古老INT96时间戳时失败。
CORRECTED:加载INT96时间戳而不重新基准。
LEGACY:将古老时间戳从儒略历重新基准到公历。
读取
mergeSchemaspark.sql.parquet.mergeSchema配置的值设置是否应合并从所有Parquet部分文件收集的模式。这将覆盖spark.sql.parquet.mergeSchema。读取
compressionsnappy保存到文件时使用的压缩编解码器。这可以是已知的不区分大小写的短名称之一(none、uncompressed、snappy、gzip、lzo、brotli、lz4、zstd)。这将覆盖spark.sql.parquet.compression.codec。写入

其他通用选项可以在通用文件源选项中找到。

配置

可以使用SparkSession上的setConf方法或通过使用SQL运行SET key=value命令来完成Parquet的配置。

属性名称默认值含义自版本
spark.sql.parquet.binaryAsStringfalse一些其他产生Parquet的系统,特别是Impala、Hive和旧版本的Spark SQL,在写出Parquet模式时不区分二进制数据和字符串。此标志告诉Spark SQL将二进制数据解释为字符串以提供与这些系统的兼容性。1.1.1
spark.sql.parquet.int96AsTimestamptrue一些产生Parquet的系统,特别是Impala和Hive,将时间戳存储到INT96中。此标志告诉Spark SQL将INT96数据解释为时间戳以提供与这些系统的兼容性。1.3.0
spark.sql.parquet.int96TimestampConversionfalse当将Impala写入的数据转换为时间戳时,此控件是否应对INT96数据应用时间戳调整。这是必要的,因为Impala存储的INT96数据具有与Hive和Spark不同的时区偏移。2.3.0
spark.sql.parquet.outputTimestampTypeINT96设置Spark将数据写入Parquet文件时使用的Parquet时间戳类型。INT96是Parquet中非标准但常用的时间戳类型。TIMESTAMP_MICROS是Parquet中的标准时间戳类型,存储从Unix纪元开始的微秒数。TIMESTAMP_MILLIS也是标准的,但具有毫秒精度,这意味着Spark必须截断其时间戳值的微秒部分。2.3.0
spark.sql.parquet.compression.codecsnappy设置写入Parquet文件时使用的压缩编解码器。如果在表特定选项/属性中指定了compression或parquet.compression,则优先级为compression、parquet.compression、spark.sql.parquet.compression.codec。可接受的值包括:none、uncompressed、snappy、gzip、lzo、brotli、lz4、zstd。注意brotli需要安装BrotliCodec。1.1.1
spark.sql.parquet.filterPushdowntrue当设置为true时,启用Parquet过滤器下推优化。1.2.0
spark.sql.parquet.aggregatePushdownfalse如果为true,聚合将被下推到Parquet进行优化。支持MIN、MAX和COUNT作为聚合表达式。对于MIN/MAX,支持布尔值、整数、浮点数和日期类型。对于COUNT,支持所有数据类型。如果任何Parquet文件页脚中缺少统计信息,将抛出异常。3.3.0
spark.sql.hive.convertMetastoreParquettrue当设置为false时,Spark SQL将使用Hive SerDe来处理parquet表,而不是内置支持。1.1.1
spark.sql.parquet.mergeSchemafalse当为true时,Parquet数据源合并从所有数据文件收集的模式,否则模式从摘要文件选取,如果没有摘要文件可用,则从随机数据文件选取。1.5.0
spark.sql.parquet.respectSummaryFilesfalse当为true时,我们假设Parquet的所有部分文件与摘要文件一致,并在合并模式时忽略它们。否则,如果为false(默认),我们将合并所有部分文件。这应被视为仅限专家使用的选项,在确切了解其含义之前不应启用。1.5.0
spark.sql.parquet.writeLegacyFormatfalse如果为true,数据将以Spark 1.4及更早版本的方式写入。例如,十进制值将以Apache Parquet的固定长度字节数组格式写入,Apache Hive和Apache Impala等其他系统使用该格式。如果为false,将使用Parquet中的较新格式。例如,十进制值将以基于int的格式写入。如果Parquet输出用于不支持此较新格式的系统,请设置为true。1.6.0
spark.sql.parquet.enableVectorizedReadertrue启用向量化parquet解码。2.0.0
spark.sql.parquet.enableNestedColumnVectorizedReadertrue为嵌套列(例如,结构体、列表、映射)启用向量化Parquet解码。需要启用spark.sql.parquet.enableVectorizedReader。3.3.0
spark.sql.parquet.recordLevelFilter.enabledfalse如果为true,使用下推的过滤器启用Parquet的本机记录级过滤。此配置仅在启用spark.sql.parquet.filterPushdown且未使用向量化读取器时有效。您可以通过将spark.sql.parquet.enableVectorizedReader设置为false来确保不使用向量化读取器。2.3.0
spark.sql.parquet.columnarReaderBatchSize4096parquet向量化读取器批次中包含的行数。应仔细选择该数字以最小化开销并避免读取数据时出现OOM。2.4.0
spark.sql.parquet.fieldId.write.enabledtrue字段ID是Parquet模式规范的本机字段。启用后,Parquet写入器将把Spark模式中的字段ID元数据(如果存在)填充到Parquet模式中。3.3.0
spark.sql.parquet.fieldId.read.enabledfalse字段ID是Parquet模式规范的本机字段。启用后,Parquet读取器将使用请求的Spark模式中的字段ID(如果存在)来查找Parquet字段,而不是使用列名。3.3.0
spark.sql.parquet.fieldId.read.ignoreMissingfalse当Parquet文件没有任何字段ID但Spark读取模式使用字段ID进行读取时,如果启用此标志,我们将静默返回null,否则报错。3.3.0
spark.sql.parquet.inferTimestampNTZ.enabledtrue启用后,在模式推断期间,带有注解isAdjustedToUTC = false的Parquet时间戳列被推断为TIMESTAMP_NTZ类型。否则,所有Parquet时间戳列都被推断为TIMESTAMP_LTZ类型。注意Spark在文件写入时将输出模式写入Parquet的页脚元数据,并在文件读取时利用它。因此,此配置仅影响未由Spark写入的Parquet文件的模式推断。3.4.0
spark.sql.parquet.datetimeRebaseModeInReadEXCEPTION从儒略历到公历的DATE、TIMESTAMP_MILLIS、TIMESTAMP_MICROS逻辑类型值的重新基准模式:
EXCEPTION:如果Spark看到两个日历之间不明确的古老日期/时间戳,将使读取失败。
CORRECTED:Spark不会重新基准,并按原样读取日期/时间戳。
LEGACY:在读取Parquet文件时,Spark将日期/时间戳从传统的混合(儒略+公历)日历重新基准到公历。
此配置仅在Parquet文件的写入者信息(如Spark、Hive)未知时有效。
3.0.0
spark.sql.parquet.datetimeRebaseModeInWriteEXCEPTION从公历到儒略历的DATE、TIMESTAMP_MILLIS、TIMESTAMP_MICROS逻辑类型值的重新基准模式:
EXCEPTION:如果Spark看到两个日历之间不明确的古老日期/时间戳,将使写入失败。
CORRECTED:Spark不会重新基准,并按原样写入日期/时间戳。
LEGACY:在写入Parquet文件时,Spark将日期/时间戳从公历日历重新基准到传统的混合(儒略+公历)日历。
3.0.0
spark.sql.parquet.int96RebaseModeInReadEXCEPTION从儒略历到公历的INT96时间戳类型值的重新基准模式:
EXCEPTION:如果Spark看到两个日历之间不明确的古老INT96时间戳,将使读取失败。
CORRECTED:Spark不会重新基准,并按原样读取日期/时间戳。
LEGACY:在读取Parquet文件时,Spark将INT96时间戳从传统的混合(儒略+公历)日历重新基准到公历。
此配置仅在Parquet文件的写入者信息(如Spark、Hive)未知时有效。
3.1.0
spark.sql.parquet.int96RebaseModeInWriteEXCEPTION从公历到儒略历的INT96时间戳类型值的重新基准模式:
EXCEPTION:如果Spark看到两个日历之间不明确的古老时间戳,将使写入失败。
CORRECTED:Spark不会重新基准,并按原样写入日期/时间戳。
LEGACY:在写入Parquet文件时,Spark将INT96时间戳从公历日历重新基准到传统的混合(儒略+公历)日历。
3.1.0
Scala 文件
以编程方式加载数据

使用上述示例中的数据:

// 大多数常见类型的编码器通过导入spark.implicits._自动提供
import spark.implicits._val peopleDF = spark.read.json("examples/src/main/resources/people.json")// DataFrame可以保存为Parquet文件,保留模式信息
peopleDF.write.parquet("people.parquet")// 读取上面创建的parquet文件
// Parquet文件是自描述的,因此模式被保留
// 加载Parquet文件的结果也是一个DataFrame
val parquetFileDF = spark.read.parquet("people.parquet")// Parquet文件也可以用于创建临时视图,然后在SQL语句中使用
parquetFileDF.createOrReplaceTempView("parquetFile")
val namesDF = spark.sql("SELECT name FROM parquetFile WHERE age BETWEEN 13 AND 19")
namesDF.map(attributes => "Name: " + attributes(0)).show()
// +------------+
// |       value|
// +------------+
// |Name: Justin|
// +------------+

完整示例代码可在Spark代码库的"examples/src/main/scala/org/apache/spark/examples/sql/SQLDataSourceExample.scala"中找到。

分区发现

表分区是Hive等系统中常用的优化方法。在分区表中,数据通常存储在不同的目录中,分区列值编码在每个分区目录的路径中。所有内置文件源(包括Text/CSV/JSON/ORC/Parquet)都能够自动发现和推断分区信息。例如,我们可以使用以下目录结构将之前使用的人口数据存储到分区表中,其中两个额外的列gendercountry作为分区列:

path
└── to└── table├── gender=male│   ├── ...│   ││   ├── country=US│   │   └── data.parquet│   ├── country=CN│   │   └── data.parquet│   └── ...└── gender=female├── ...│├── country=US│   └── data.parquet├── country=CN│   └── data.parquet└── ...

通过将path/to/table传递给SparkSession.read.parquetSparkSession.read.load,Spark SQL将自动从路径中提取分区信息。现在返回的DataFrame的模式变为:

root
|-- name: string (nullable = true)
|-- age: long (nullable = true)
|-- gender: string (nullable = true)
|-- country: string (nullable = true)

请注意,分区列的数据类型是自动推断的。目前支持数值数据类型、日期、时间戳和字符串类型。有时用户可能不希望自动推断分区列的数据类型。对于这些用例,可以通过spark.sql.sources.partitionColumnTypeInference.enabled配置自动类型推断,该配置默认为true。当类型推断被禁用时,分区列将使用字符串类型。

从Spark 1.6.0开始,默认情况下分区发现只查找给定路径下的分区。对于上面的例子,如果用户将path/to/table/gender=male传递给SparkSession.read.parquetSparkSession.read.loadgender将不会被视作分区列。如果用户需要指定分区发现应该开始的基路径,可以在数据源选项中设置basePath。例如,当path/to/table/gender=male是数据路径且用户将basePath设置为path/to/table/时,gender将是一个分区列。

模式合并

与Protocol Buffer、Avro和Thrift类似,Parquet也支持模式演化。用户可以从一个简单的模式开始,然后根据需要逐渐向模式添加更多列。这样,用户最终可能会得到多个具有不同但相互兼容模式的Parquet文件。Parquet数据源现在能够自动检测这种情况并合并所有这些文件的模式。

由于模式合并是一个相对昂贵的操作,并且在大多数情况下不是必需的,我们从1.5.0开始默认关闭了它。您可以通过以下方式启用它:

  • 在读取Parquet文件时设置数据源选项mergeSchema为true(如下面的示例所示),或
  • 设置全局SQL选项spark.sql.parquet.mergeSchema为true。
// 这用于隐式将RDD转换为DataFrame
import spark.implicits._// 创建一个简单的DataFrame,存储到分区目录
val squaresDF = spark.sparkContext.makeRDD(1 to 5).map(i => (i, i * i)).toDF("value", "square")
squaresDF.write.parquet("data/test_table/key=1")// 在新分区目录中创建另一个DataFrame,
// 添加新列并删除现有列
val cubesDF = spark.sparkContext.makeRDD(6 to 10).map(i => (i, i * i * i)).toDF("value", "cube")
cubesDF.write.parquet("data/test_table/key=2")// 读取分区表
val mergedDF = spark.read.option("mergeSchema", "true").parquet("data/test_table")
mergedDF.printSchema()// 最终模式包含Parquet文件中的所有3列
// 以及出现在分区目录路径中的分区列
// root
//  |-- value: int (nullable = true)
//  |-- square: int (nullable = true)
//  |-- cube: int (nullable = true)
//  |-- key: int (nullable = true)

完整示例代码可在Spark代码库的"examples/src/main/scala/org/apache/spark/examples/sql/SQLDataSourceExample.scala"中找到。

Hive元存储Parquet表转换

当从Hive元存储Parquet表读取数据并写入到非分区的Hive元存储Parquet表时,Spark SQL将尝试使用自己的Parquet支持而不是Hive SerDe以获得更好的性能。此行为由spark.sql.hive.convertMetastoreParquet配置控制,默认开启。

Hive/Parquet模式协调

从表模式处理的角度来看,Hive和Parquet有两个关键区别:

  1. Hive不区分大小写,而Parquet区分
  2. Hive认为所有列都可为空,而Parquet中的可空性很重要

由于这个原因,在将Hive元存储Parquet表转换为Spark SQL Parquet表时,我们必须协调Hive元存储模式和Parquet模式。协调规则是:

  • 在两个模式中具有相同名称的字段必须具有相同的数据类型,无论可空性如何。协调后的字段应具有Parquet端的数据类型,以便尊重可空性。
  • 协调后的模式恰好包含Hive元存储模式中定义的字段。
    • 任何仅在Parquet模式中出现的字段在协调后的模式中被丢弃。
    • 任何仅在Hive元存储模式中出现的字段在协调后的模式中作为可空字段添加。
元数据刷新

Spark SQL会缓存Parquet元数据以提高性能。当启用Hive元存储Parquet表转换时,这些转换表的元数据也会被缓存。如果这些表被Hive或其他外部工具更新,您需要手动刷新它们以确保元数据一致。

// spark是现有的SparkSession
spark.catalog.refreshTable("my_table")
列式加密

从Spark 3.2开始,支持使用Apache Parquet 1.12+的Parquet表的列式加密。

Parquet使用信封加密实践,其中文件部分使用"数据加密密钥"(DEK)加密,而DEK使用"主加密密钥"(MEK)加密。DEK由Parquet为每个加密文件/列随机生成。MEK在用户选择的密钥管理服务(KMS)中生成、存储和管理。Parquet Maven仓库有一个包含模拟KMS实现的jar,允许仅使用spark-shell运行列加密和解密,而无需部署KMS服务器(下载parquet-hadoop-tests.jar文件并将其放在Spark jars文件夹中):

sc.hadoopConfiguration.set("parquet.encryption.kms.client.class" ,"org.apache.parquet.crypto.keytools.mocks.InMemoryKMS")// 显式主密钥(base64编码)- 仅模拟InMemoryKMS需要
sc.hadoopConfiguration.set("parquet.encryption.key.list" ,"keyA:AAECAwQFBgcICQoLDA0ODw== ,  keyB:AAECAAECAAECAAECAAECAA==")// 激活Parquet加密,由Hadoop属性驱动
sc.hadoopConfiguration.set("parquet.crypto.factory.class" ,"org.apache.parquet.crypto.keytools.PropertiesDrivenCryptoFactory")// 写入加密的dataframe文件
// 列"square"将使用主密钥"keyA"保护
// Parquet文件页脚将使用主密钥"keyB"保护
squaresDF.write.option("parquet.encryption.column.keys" , "keyA:square").option("parquet.encryption.footer.key" , "keyB").
parquet("/path/to/table.parquet.encrypted")// 读取加密的dataframe文件
val df2 = spark.read.parquet("/path/to/table.parquet.encrypted")
KMS客户端

InMemoryKMS类仅用于说明和简单演示Parquet加密功能,不应在实际部署中使用。主加密密钥必须保存在用户组织中部署的生产级KMS系统中。推出带有Parquet加密的Spark需要为KMS服务器实现一个客户端类。Parquet提供了一个插件接口用于开发此类类:

public interface KmsClient {// 包装密钥 - 使用主密钥加密它public String wrapKey(byte[] keyBytes, String masterKeyIdentifier);// 使用主密钥解密(解包)密钥public byte[] unwrapKey(String wrappedKey, String masterKeyIdentifier);// 初始化参数的使用是可选的public void initialize(Configuration configuration, String kmsInstanceID,String kmsInstanceURL, String accessToken);
}

可以在parquet-mr仓库中找到此类类的开源KMS示例。生产KMS客户端应与组织的安全管理员合作设计,并由具有访问控制管理经验的开发人员构建。一旦创建了这样的类,就可以通过parquet.encryption.kms.client.class参数传递给应用程序,并由普通Spark用户利用,如上所示的加密dataframe写入/读取示例。

注意:默认情况下,Parquet实现了"双重信封加密"模式,最大限度地减少了Spark执行器与KMS服务器的交互。在此模式下,DEK使用"密钥加密密钥"(KEK,由Parquet随机生成)加密。KEK在KMS中使用MEK加密;结果和KEK本身缓存在Spark执行器内存中。对常规信封加密感兴趣的用户可以通过将parquet.encryption.double.wrapping参数设置为false来切换到它。有关Parquet加密参数的更多详细信息,请访问parquet-hadoop配置页面。

数据源选项

Parquet的数据源选项可以通过以下方式设置:

  • DataFrameReaderDataFrameWriterDataStreamReaderDataStreamWriter.option/.options方法
  • CREATE TABLE USING DATA_SOURCE中的OPTIONS子句
属性名称默认值含义作用范围
datetimeRebaseModespark.sql.parquet.datetimeRebaseModeInRead配置的值datetimeRebaseMode选项允许指定从儒略历到公历的DATE、TIMESTAMP_MILLIS、TIMESTAMP_MICROS逻辑类型值的重新基准模式。
当前支持的模式有:
EXCEPTION:在读取两个日历之间不明确的古老日期/时间戳时失败。
CORRECTED:加载日期/时间戳而不重新基准。
LEGACY:将古老日期/时间戳从儒略历重新基准到公历。
读取
int96RebaseModespark.sql.parquet.int96RebaseModeInRead配置的值int96RebaseMode选项允许指定从儒略历到公历的INT96时间戳的重新基准模式。
当前支持的模式有:
EXCEPTION:在读取两个日历之间不明确的古老INT96时间戳时失败。
CORRECTED:加载INT96时间戳而不重新基准。
LEGACY:将古老时间戳从儒略历重新基准到公历。
读取
mergeSchemaspark.sql.parquet.mergeSchema配置的值设置是否应合并从所有Parquet部分文件收集的模式。这将覆盖spark.sql.parquet.mergeSchema。读取
compressionsnappy保存到文件时使用的压缩编解码器。这可以是已知的不区分大小写的短名称之一(none、uncompressed、snappy、gzip、lzo、brotli、lz4、zstd)。这将覆盖spark.sql.parquet.compression.codec。写入

其他通用选项可以在通用文件源选项中找到。

配置

可以使用SparkSession上的setConf方法或通过使用SQL运行SET key=value命令来完成Parquet的配置。

属性名称默认值含义自版本
spark.sql.parquet.binaryAsStringfalse一些其他产生Parquet的系统,特别是Impala、Hive和旧版本的Spark SQL,在写出Parquet模式时不区分二进制数据和字符串。此标志告诉Spark SQL将二进制数据解释为字符串以提供与这些系统的兼容性。1.1.1
spark.sql.parquet.int96AsTimestamptrue一些产生Parquet的系统,特别是Impala和Hive,将时间戳存储到INT96中。此标志告诉Spark SQL将INT96数据解释为时间戳以提供与这些系统的兼容性。1.3.0
spark.sql.parquet.int96TimestampConversionfalse当将Impala写入的数据转换为时间戳时,此控件是否应对INT96数据应用时间戳调整。这是必要的,因为Impala存储的INT96数据具有与Hive和Spark不同的时区偏移。2.3.0
spark.sql.parquet.outputTimestampTypeINT96设置Spark将数据写入Parquet文件时使用的Parquet时间戳类型。INT96是Parquet中非标准但常用的时间戳类型。TIMESTAMP_MICROS是Parquet中的标准时间戳类型,存储从Unix纪元开始的微秒数。TIMESTAMP_MILLIS也是标准的,但具有毫秒精度,这意味着Spark必须截断其时间戳值的微秒部分。2.3.0
spark.sql.parquet.compression.codecsnappy设置写入Parquet文件时使用的压缩编解码器。如果在表特定选项/属性中指定了compression或parquet.compression,则优先级为compression、parquet.compression、spark.sql.parquet.compression.codec。可接受的值包括:none、uncompressed、snappy、gzip、lzo、brotli、lz4、zstd。注意brotli需要安装BrotliCodec。1.1.1
spark.sql.parquet.filterPushdowntrue当设置为true时,启用Parquet过滤器下推优化。1.2.0
spark.sql.parquet.aggregatePushdownfalse如果为true,聚合将被下推到Parquet进行优化。支持MIN、MAX和COUNT作为聚合表达式。对于MIN/MAX,支持布尔值、整数、浮点数和日期类型。对于COUNT,支持所有数据类型。如果任何Parquet文件页脚中缺少统计信息,将抛出异常。3.3.0
spark.sql.hive.convertMetastoreParquettrue当设置为false时,Spark SQL将使用Hive SerDe来处理parquet表,而不是内置支持。1.1.1
spark.sql.parquet.mergeSchemafalse当为true时,Parquet数据源合并从所有数据文件收集的模式,否则模式从摘要文件选取,如果没有摘要文件可用,则从随机数据文件选取。1.5.0
spark.sql.parquet.respectSummaryFilesfalse当为true时,我们假设Parquet的所有部分文件与摘要文件一致,并在合并模式时忽略它们。否则,如果为false(默认),我们将合并所有部分文件。这应被视为仅限专家使用的选项,在确切了解其含义之前不应启用。1.5.0
spark.sql.parquet.writeLegacyFormatfalse如果为true,数据将以Spark 1.4及更早版本的方式写入。例如,十进制值将以Apache Parquet的固定长度字节数组格式写入,Apache Hive和Apache Impala等其他系统使用该格式。如果为false,将使用Parquet中的较新格式。例如,十进制值将以基于int的格式写入。如果Parquet输出用于不支持此较新格式的系统,请设置为true。1.6.0
spark.sql.parquet.enableVectorizedReadertrue启用向量化parquet解码。2.0.0
spark.sql.parquet.enableNestedColumnVectorizedReadertrue为嵌套列(例如,结构体、列表、映射)启用向量化Parquet解码。需要启用spark.sql.parquet.enableVectorizedReader。3.3.0
spark.sql.parquet.recordLevelFilter.enabledfalse如果为true,使用下推的过滤器启用Parquet的本机记录级过滤。此配置仅在启用spark.sql.parquet.filterPushdown且未使用向量化读取器时有效。您可以通过将spark.sql.parquet.enableVectorizedReader设置为false来确保不使用向量化读取器。2.3.0
spark.sql.parquet.columnarReaderBatchSize4096parquet向量化读取器批次中包含的行数。应仔细选择该数字以最小化开销并避免读取数据时出现OOM。2.4.0
spark.sql.parquet.fieldId.write.enabledtrue字段ID是Parquet模式规范的本机字段。启用后,Parquet写入器将把Spark模式中的字段ID元数据(如果存在)填充到Parquet模式中。3.3.0
spark.sql.parquet.fieldId.read.enabledfalse字段ID是Parquet模式规范的本机字段。启用后,Parquet读取器将使用请求的Spark模式中的字段ID(如果存在)来查找Parquet字段,而不是使用列名。3.3.0
spark.sql.parquet.fieldId.read.ignoreMissingfalse当Parquet文件没有任何字段ID但Spark读取模式使用字段ID进行读取时,如果启用此标志,我们将静默返回null,否则报错。3.3.0
spark.sql.parquet.inferTimestampNTZ.enabledtrue启用后,在模式推断期间,带有注解isAdjustedToUTC = false的Parquet时间戳列被推断为TIMESTAMP_NTZ类型。否则,所有Parquet时间戳列都被推断为TIMESTAMP_LTZ类型。注意Spark在文件写入时将输出模式写入Parquet的页脚元数据,并在文件读取时利用它。因此,此配置仅影响未由Spark写入的Parquet文件的模式推断。3.4.0
spark.sql.parquet.datetimeRebaseModeInReadEXCEPTION从儒略历到公历的DATE、TIMESTAMP_MILLIS、TIMESTAMP_MICROS逻辑类型值的重新基准模式:
EXCEPTION:如果Spark看到两个日历之间不明确的古老日期/时间戳,将使读取失败。
CORRECTED:Spark不会重新基准,并按原样读取日期/时间戳。
LEGACY:在读取Parquet文件时,Spark将日期/时间戳从传统的混合(儒略+公历)日历重新基准到公历。
此配置仅在Parquet文件的写入者信息(如Spark、Hive)未知时有效。
3.0.0
spark.sql.parquet.datetimeRebaseModeInWriteEXCEPTION从公历到儒略历的DATE、TIMESTAMP_MILLIS、TIMESTAMP_MICROS逻辑类型值的重新基准模式:
EXCEPTION:如果Spark看到两个日历之间不明确的古老日期/时间戳,将使写入失败。
CORRECTED:Spark不会重新基准,并按原样写入日期/时间戳。
LEGACY:在写入Parquet文件时,Spark将日期/时间戳从公历日历重新基准到传统的混合(儒略+公历)日历。
3.0.0
spark.sql.parquet.int96RebaseModeInReadEXCEPTION从儒略历到公历的INT96时间戳类型值的重新基准模式:
EXCEPTION:如果Spark看到两个日历之间不明确的古老INT96时间戳,将使读取失败。
CORRECTED:Spark不会重新基准,并按原样读取日期/时间戳。
LEGACY:在读取Parquet文件时,Spark将INT96时间戳从传统的混合(儒略+公历)日历重新基准到公历。
此配置仅在Parquet文件的写入者信息(如Spark、Hive)未知时有效。
3.1.0
spark.sql.parquet.int96RebaseModeInWriteEXCEPTION从公历到儒略历的INT96时间戳类型值的重新基准模式:
EXCEPTION:如果Spark看到两个日历之间不明确的古老时间戳,将使写入失败。
CORRECTED:Spark不会重新基准,并按原样写入日期/时间戳。
LEGACY:在写入Parquet文件时,Spark将INT96时间戳从公历日历重新基准到传统的混合(儒略+公历)日历。
3.1.0
Java 文件
编程方式加载数据

使用上述示例中的数据:

import org.apache.spark.api.java.function.MapFunction;
import org.apache.spark.sql.Encoders;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;Dataset<Row> peopleDF = spark.read().json("examples/src/main/resources/people.json");// DataFrame可以保存为Parquet文件,同时保留模式信息
peopleDF.write().parquet("people.parquet");// 读取上面创建的Parquet文件
// Parquet文件是自描述的,因此模式信息得以保留
// 加载Parquet文件的结果也是一个DataFrame
Dataset<Row> parquetFileDF = spark.read().parquet("people.parquet");// Parquet文件也可以用来创建临时视图,然后在SQL语句中使用
parquetFileDF.createOrReplaceTempView("parquetFile");
Dataset<Row> namesDF = spark.sql("SELECT name FROM parquetFile WHERE age BETWEEN 13 AND 19");
Dataset<String> namesDS = namesDF.map((MapFunction<Row, String>) row -> "Name: " + row.getString(0),Encoders.STRING());
namesDS.show();
// +------------+
// |       value|
// +------------+
// |Name: Justin|
// +------------+

完整示例代码请参见Spark代码库中的"examples/src/main/java/org/apache/spark/examples/sql/JavaSQLDataSourceExample.java"。

分区发现

表分区是Hive等系统中常用的优化方法。在分区表中,数据通常存储在不同的目录中,分区列的值编码在每个分区目录的路径中。所有内置的文件源(包括Text/CSV/JSON/ORC/Parquet)都能自动发现和推断分区信息。例如,我们可以使用以下目录结构将之前使用的人口数据存储到分区表中,其中两个额外的列gendercountry作为分区列:

path
└── to└── table├── gender=male│   ├── ...│   ││   ├── country=US│   │   └── data.parquet│   ├── country=CN│   │   └── data.parquet│   └── ...└── gender=female├── ...│├── country=US│   └── data.parquet├── country=CN│   └── data.parquet└── ...

通过将path/to/table传递给SparkSession.read.parquetSparkSession.read.load,Spark SQL将自动从路径中提取分区信息。现在返回的DataFrame的模式变为:

root
|-- name: string (nullable = true)
|-- age: long (nullable = true)
|-- gender: string (nullable = true)
|-- country: string (nullable = true)

注意,分区列的数据类型是自动推断的。目前支持数值类型、日期、时间戳和字符串类型。有时用户可能不希望自动推断分区列的数据类型。对于这些用例,可以通过配置spark.sql.sources.partitionColumnTypeInference.enabled来禁用自动类型推断,该配置默认为true。当类型推断被禁用时,分区列将使用字符串类型。

从Spark 1.6.0开始,默认情况下分区发现只会在给定路径下查找分区。对于上面的示例,如果用户将path/to/table/gender=male传递给SparkSession.read.parquetSparkSession.read.loadgender将不会被视作分区列。如果用户需要指定分区发现应从哪个基础路径开始,可以在数据源选项中设置basePath。例如,当数据路径为path/to/table/gender=male且用户将basePath设置为path/to/table/时,gender将成为一个分区列。

模式合并

与Protocol Buffer、Avro和Thrift一样,Parquet也支持模式演化。用户可以从一个简单的模式开始,并根据需要逐步向模式中添加更多列。这样,用户最终可能会得到多个具有不同但相互兼容模式的Parquet文件。Parquet数据源现在能够自动检测这种情况,并合并所有这些文件的模式。

由于模式合并是一个相对昂贵的操作,并且在大多数情况下不是必需的,因此从1.5.0开始默认关闭。您可以通过以下方式启用它:

  • 在读取Parquet文件时设置数据源选项mergeSchematrue(如下面的示例所示),或
  • 设置全局SQL选项spark.sql.parquet.mergeSchematrue
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;public static class Square implements Serializable {private int value;private int square;// Getter和setter方法...
}public static class Cube implements Serializable {private int value;private int cube;// Getter和setter方法...
}List<Square> squares = new ArrayList<>();
for (int value = 1; value <= 5; value++) {Square square = new Square();square.setValue(value);square.setSquare(value * value);squares.add(square);
}// 创建一个简单的DataFrame,存储到一个分区目录
Dataset<Row> squaresDF = spark.createDataFrame(squares, Square.class);
squaresDF.write().parquet("data/test_table/key=1");List<Cube> cubes = new ArrayList<>();
for (int value = 6; value <= 10; value++) {Cube cube = new Cube();cube.setValue(value);cube.setCube(value * value * value);cubes.add(cube);
}// 在另一个分区目录中创建另一个DataFrame,
// 添加一个新列并删除一个现有列
Dataset<Row> cubesDF = spark.createDataFrame(cubes, Cube.class);
cubesDF.write().parquet("data/test_table/key=2");// 读取分区表
Dataset<Row> mergedDF = spark.read().option("mergeSchema", true).parquet("data/test_table");
mergedDF.printSchema();// 最终模式由Parquet文件中的所有3列组成,
// 加上出现在分区目录路径中的分区列
// root
//  |-- value: int (nullable = true)
//  |-- square: int (nullable = true)
//  |-- cube: int (nullable = true)
//  |-- key: int (nullable = true)

完整示例代码请参见Spark代码库中的"examples/src/main/java/org/apache/spark/examples/sql/JavaSQLDataSourceExample.java"。

Hive元存储Parquet表转换

当从Hive元存储Parquet表读取数据并向非分区的Hive元存储Parquet表写入数据时,Spark SQL会尝试使用自己的Parquet支持而不是Hive SerDe以获得更好的性能。此行为由spark.sql.hive.convertMetastoreParquet配置控制,默认开启。

Hive/Parquet模式协调

从表模式处理的角度来看,Hive和Parquet有两个关键区别:

  1. Hive不区分大小写,而Parquet区分大小写
  2. Hive认为所有列都可为空,而Parquet中的空值约束是重要的

因此,在将Hive元存储Parquet表转换为Spark SQL Parquet表时,必须协调Hive元存储模式与Parquet模式。协调规则如下:

  • 两个模式中同名字段必须具有相同的数据类型,无论是否可为空。协调后的字段应具有Parquet侧的数据类型,以遵守空值约束。
  • 协调后的模式仅包含Hive元存储模式中定义的字段。
  • 任何仅出现在Parquet模式中的字段在协调后的模式中将被丢弃。
  • 任何仅出现在Hive元存储模式中的字段在协调后的模式中将作为可为空字段添加。
元数据刷新

Spark SQL会缓存Parquet元数据以提高性能。当启用Hive元存储Parquet表转换时,这些转换后的表的元数据也会被缓存。如果这些表被Hive或其他外部工具更新,您需要手动刷新它们以确保元数据的一致性。

// spark是一个现有的SparkSession
spark.catalog().refreshTable("my_table");
列式加密

从Spark 3.2开始,支持对使用Apache Parquet 1.12+的Parquet表进行列式加密。

Parquet使用信封加密实践,其中文件部分使用"数据加密密钥"(DEK)加密,而DEK使用"主加密密钥"(MEK)加密。DEK由Parquet为每个加密的文件/列随机生成。MEK在用户选择的密钥管理服务(KMS)中生成、存储和管理。Parquet Maven仓库提供了一个包含模拟KMS实现的jar,允许仅使用spark-shell运行列加密和解密,而无需部署KMS服务器(下载parquet-hadoop-tests.jar文件并将其放入Spark的jars文件夹中):

sc.hadoopConfiguration().set("parquet.encryption.kms.client.class","org.apache.parquet.crypto.keytools.mocks.InMemoryKMS");// 显式的主密钥(base64编码)- 仅用于模拟的InMemoryKMS
sc.hadoopConfiguration().set("parquet.encryption.key.list","keyA:AAECAwQFBgcICQoLDA0ODw== ,  keyB:AAECAAECAAECAAECAAECAA==");// 激活Parquet加密,由Hadoop属性驱动
sc.hadoopConfiguration().set("parquet.crypto.factory.class","org.apache.parquet.crypto.keytools.PropertiesDrivenCryptoFactory");// 写入加密的DataFrame文件。
// 列"square"将用主密钥"keyA"保护。
// Parquet文件页脚将用主密钥"keyB"保护
squaresDF.write().option("parquet.encryption.column.keys", "keyA:square").option("parquet.encryption.footer.key", "keyB").parquet("/path/to/table.parquet.encrypted");// 读取加密的DataFrame文件
Dataset<Row> df2 = spark.read().parquet("/path/to/table.parquet.encrypted");
KMS客户端

InMemoryKMS类仅用于说明和简单演示Parquet加密功能,不应在实际部署中使用。主加密密钥必须在用户组织内部署的生产级KMS系统中保存和管理。推出带有Parquet加密的Spark需要为KMS服务器实现一个客户端类。Parquet提供了一个插件接口用于开发此类类:

public interface KmsClient {// 包装一个密钥 - 用主密钥加密它。public String wrapKey(byte[] keyBytes, String masterKeyIdentifier);// 用主密钥解密(解包)一个密钥。public byte[] unwrapKey(String wrappedKey, String masterKeyIdentifier);// 使用初始化参数是可选的。public void initialize(Configuration configuration, String kmsInstanceID,String kmsInstanceURL, String accessToken);
}

可以在parquet-mr仓库中找到此类类的开源KMS示例。生产KMS客户端应与组织的安全管理员合作设计,并由具有访问控制管理经验的开发人员构建。一旦创建了这样的类,就可以通过parquet.encryption.kms.client.class参数传递给应用程序,并由普通Spark用户使用,如上文的加密DataFrame写入/读取示例所示。

注意:默认情况下,Parquet实现了一种"双重信封加密"模式,最大限度地减少了Spark执行器与KMS服务器的交互。在这种模式下,DEK使用"密钥加密密钥"(KEK,由Parquet随机生成)进行加密。KEK在KMS中使用MEK加密;结果和KEK本身缓存在Spark执行器内存中。对常规信封加密感兴趣的用户可以通过将parquet.encryption.double.wrapping参数设置为false来切换到该模式。有关Parquet加密参数的更多详细信息,请访问parquet-hadoop配置页面。

数据源选项

Parquet的数据源选项可以通过以下方式设置:

  • DataFrameReaderDataFrameWriterDataStreamReaderDataStreamWriter.option/.options方法
  • CREATE TABLE USING DATA_SOURCE语句中的OPTIONS子句
属性名称默认值含义作用范围
datetimeRebaseModespark.sql.parquet.datetimeRebaseModeInRead配置的值datetimeRebaseMode选项允许为从儒略历到公历的DATETIMESTAMP_MILLISTIMESTAMP_MICROS逻辑类型的值指定重新基准模式。
当前支持的模式有:
- EXCEPTION:在读取两个日历之间不明确的古老日期/时间戳时失败。
- CORRECTED:不重新基准化,直接加载日期/时间戳。
- LEGACY:将古老日期/时间戳从儒略历重新基准到公历。
读取
int96RebaseModespark.sql.parquet.int96RebaseModeInRead配置的值int96RebaseMode选项允许为从儒略历到公历的INT96时间戳指定重新基准模式。
当前支持的模式有:
- EXCEPTION:在读取两个日历之间不明确的古老INT96时间戳时失败。
- CORRECTED:不重新基准化,直接加载INT96时间戳。
- LEGACY:将古老时间戳从儒略历重新基准到公历。
读取
mergeSchemaspark.sql.parquet.mergeSchema配置的值设置是否应合并从所有Parquet部分文件收集的模式。这将覆盖spark.sql.parquet.mergeSchema读取
compressionsnappy保存到文件时使用的压缩编解码器。可以是已知的不区分大小写的简称(noneuncompressedsnappygziplzobrotlilz4zstd)。这将覆盖spark.sql.parquet.compression.codec写入

其他通用选项可以在通用文件源选项中找到。

配置

Parquet的配置可以使用SparkSession上的setConf方法或通过使用SQL运行SET key=value命令来完成。

属性名称默认值含义自版本
spark.sql.parquet.binaryAsStringfalse一些其他生成Parquet的系统,特别是Impala、Hive和旧版本的Spark SQL,在写出Parquet模式时不区分二进制数据和字符串。此标志告诉Spark SQL将二进制数据解释为字符串以提供与这些系统的兼容性。1.1.1
spark.sql.parquet.int96AsTimestamptrue一些生成Parquet的系统,特别是Impala和Hive,将时间戳存储到INT96中。此标志告诉Spark SQL将INT96数据解释为时间戳以提供与这些系统的兼容性。1.3.0
spark.sql.parquet.int96TimestampConversionfalse当将Impala写入的数据转换为时间戳时,此控制是否应对INT96数据应用时间戳调整。这是必要的,因为Impala存储的INT96数据与Hive和Spark的时区偏移不同。2.3.0
spark.sql.parquet.outputTimestampTypeINT96设置Spark将数据写入Parquet文件时使用的Parquet时间戳类型。INT96是Parquet中非标准但常用的时间戳类型。TIMESTAMP_MICROS是Parquet中的标准时间戳类型,存储自Unix纪元以来的微秒数。TIMESTAMP_MILLIS也是标准类型,但具有毫秒精度,这意味着Spark必须截断其时间戳值的微秒部分。2.3.0
spark.sql.parquet.compression.codecsnappy设置写入Parquet文件时使用的压缩编解码器。如果在表特定选项/属性中指定了compressionparquet.compression,优先级将是compressionparquet.compressionspark.sql.parquet.compression.codec。可接受的值包括:noneuncompressedsnappygziplzobrotlilz4zstd。注意,brotli需要安装BrotliCodec1.1.1
spark.sql.parquet.filterPushdowntrue设置为true时启用Parquet过滤器下推优化。1.2.0
spark.sql.parquet.aggregatePushdownfalse如果为true,聚合将被下推到Parquet进行优化。支持MINMAXCOUNT作为聚合表达式。对于MIN/MAX,支持布尔值、整数、浮点数和日期类型。对于COUNT,支持所有数据类型。如果任何Parquet文件页脚中缺少统计信息,将抛出异常。3.3.0
spark.sql.hive.convertMetastoreParquettrue当设置为false时,Spark SQL将对parquet表使用Hive SerDe而不是内置支持。1.1.1
spark.sql.parquet.mergeSchemafalse当为true时,Parquet数据源合并从所有数据文件收集的模式,否则模式从摘要文件或随机数据文件(如果没有摘要文件可用)中选取。1.5.0
spark.sql.parquet.respectSummaryFilesfalse当为true时,我们假设Parquet的所有部分文件与摘要文件一致,并在合并模式时忽略它们。否则,如果为false(默认值),我们将合并所有部分文件。这应被视为专家专用选项,在确切了解其含义之前不应启用。1.5.0
spark.sql.parquet.writeLegacyFormatfalse如果为true,数据将以Spark 1.4及更早版本的方式写入。例如,十进制值将以Apache Parquet的固定长度字节数组格式写入,其他系统如Apache Hive和Apache Impala使用该格式。如果为false,将使用Parquet中的新格式。例如,十进制值将基于int的格式写入。如果Parquet输出用于不支持此新格式的系统,请设置为true1.6.0
spark.sql.parquet.enableVectorizedReadertrue启用向量化parquet解码。2.0.0
spark.sql.parquet.enableNestedColumnVectorizedReadertrue为嵌套列(例如,结构体、列表、映射)启用向量化Parquet解码。需要启用spark.sql.parquet.enableVectorizedReader3.3.0
spark.sql.parquet.recordLevelFilter.enabledfalse如果为true,启用Parquet的原生记录级过滤使用下推的过滤器。此配置仅在启用spark.sql.parquet.filterPushdown且未使用向量化读取器时有效。您可以通过将spark.sql.parquet.enableVectorizedReader设置为false来确保不使用向量化读取器。2.3.0
spark.sql.parquet.columnarReaderBatchSize4096parquet向量化读取器批次中包含的行数。应仔细选择该数字以最小化开销并避免读取数据时出现OOM。2.4.0
spark.sql.parquet.fieldId.write.enabledtrue字段ID是Parquet模式规范的原生字段。启用后,Parquet写入器将把Spark模式中的字段ID元数据(如果存在)填充到Parquet模式中。3.3.0
spark.sql.parquet.fieldId.read.enabledfalse字段ID是Parquet模式规范的原生字段。启用后,Parquet读取器将使用请求的Spark模式中的字段ID(如果存在)来查找Parquet字段,而不是使用列名。3.3.0
spark.sql.parquet.fieldId.read.ignoreMissingfalse当Parquet文件没有任何字段ID但Spark读取模式使用字段ID进行读取时,如果启用此标志,我们将静默返回null,否则将报错。3.3.0
spark.sql.parquet.inferTimestampNTZ.enabledtrue启用后,带有注解isAdjustedToUTC = false的Parquet时间戳列在模式推断期间被推断为TIMESTAMP_NTZ类型。否则,所有Parquet时间戳列都被推断为TIMESTAMP_LTZ类型。注意,Spark在文件写入时将输出模式写入Parquet的页脚元数据,并在文件读取时利用它。因此,此配置仅影响未由Spark写入的Parquet文件的模式推断。3.4.0
spark.sql.parquet.datetimeRebaseModeInReadEXCEPTION从儒略历到公历的DATETIMESTAMP_MILLISTIMESTAMP_MICROS逻辑类型值的重新基准模式:
- EXCEPTION:如果Spark看到两个日历之间不明确的古老日期/时间戳,读取将失败。
- CORRECTED:Spark不进行重新基准化,按原样读取日期/时间戳。
- LEGACY:Spark在读取Parquet文件时将日期/时间戳从传统的混合(儒略历+公历)日历重新基准到公历。
此配置仅在Parquet文件的写入者信息(如Spark、Hive)未知时有效。
3.0.0
spark.sql.parquet.datetimeRebaseModeInWriteEXCEPTION从公历到儒略历的DATETIMESTAMP_MILLISTIMESTAMP_MICROS逻辑类型值的重新基准模式:
- EXCEPTION:如果Spark看到两个日历之间不明确的古老日期/时间戳,写入将失败。
- CORRECTED:Spark不进行重新基准化,按原样写入日期/时间戳。
- LEGACY:Spark在写入Parquet文件时将日期/时间戳从公历重新基准到传统的混合(儒略历+公历)日历。
3.0.0
spark.sql.parquet.int96RebaseModeInReadEXCEPTION从儒略历到公历的INT96时间戳类型的重新基准模式:
- EXCEPTION:如果Spark看到两个日历之间不明确的古老INT96时间戳,读取将失败。
- CORRECTED:Spark不进行重新基准化,按原样读取日期/时间戳。
- LEGACY:Spark在读取Parquet文件时将INT96时间戳从传统的混合(儒略历+公历)日历重新基准到公历。
此配置仅在Parquet文件的写入者信息(如Spark、Hive)未知时有效。
3.1.0
spark.sql.parquet.int96RebaseModeInWriteEXCEPTION从公历到儒略历的INT96时间戳类型的重新基准模式:
- EXCEPTION:如果Spark看到两个日历之间不明确的古老时间戳,写入将失败。
- CORRECTED:Spark不进行重新基准化,按原样写入日期/时间戳。
- LEGACY:Spark在写入Parquet文件时将INT96时间戳从公历重新基准到传统的混合(儒略历+公历)日历。
3.1.0

ORC 文件

Apache ORC 是一种列式存储格式,具备原生 Zstandard 压缩、布隆过滤器和列式加密等高级特性。

ORC 实现

Spark 支持两种 ORC 实现(原生实现和 Hive 实现),由 spark.sql.orc.impl 参数控制。两种实现共享大部分功能,但设计目标不同:

  • 原生实现 旨在遵循 Spark 数据源的行为(如 Parquet)。
  • Hive 实现 旨在遵循 Hive 的行为,并使用 Hive SerDe。

例如,早期原生实现使用 Spark 原生字符串类型处理 CHAR/VARCHAR,而 Hive 实现通过 Hive 的 CHAR/VARCHAR 处理,导致查询结果差异。自 Spark 3.1.0 起,SPARK-33480 通过 Spark 端支持 CHAR/VARCHAR 消除了这一差异。

向量化读取器

原生实现支持向量化 ORC 读取器,并自 Spark 2.3 起成为默认 ORC 实现。当 spark.sql.orc.impl 设置为 nativespark.sql.orc.enableVectorizedReadertrue 时,向量化读取器用于原生 ORC 表(例如通过 USING ORC 创建的表)。

对于 Hive ORC SerDe 表(例如通过 USING HIVE OPTIONS (fileFormat 'ORC') 创建的表),当 spark.sql.hive.convertMetastoreOrc 也设置为 true(默认开启)时,会使用向量化读取器。

Schema 合并

与 Protocol Buffer、Avro 和 Thrift 类似,ORC 也支持 Schema 演进。用户可以从简单 Schema 开始,逐步添加更多列。因此,用户可能得到多个具有不同但相互兼容 Schema 的 ORC 文件。ORC 数据源现可自动检测这种情况并合并所有文件的 Schema。

由于 Schema 合并操作开销较大且多数场景非必需,默认关闭。可通过以下方式启用:

  • 读取 ORC 文件时设置数据源选项 mergeSchematrue
  • 设置全局 SQL 选项 spark.sql.orc.mergeSchematrue
Zstandard 压缩

自 Spark 3.2 起,可在 ORC 文件中使用 Zstandard 压缩。有关优势请参考 Zstandard 文档。

CREATE TABLE compressed (key STRING,value STRING
)
USING ORC
OPTIONS (compression 'zstd'
)
布隆过滤器

可针对 ORC 数据源控制布隆过滤器和字典编码。以下示例仅对 favorite_color 列创建布隆过滤器并使用字典编码。更多 ORC 选项详见 Apache ORC 官网。

CREATE TABLE users_with_options (name STRING,favorite_color STRING,favorite_numbers array<integer>
)
USING ORC
OPTIONS (orc.bloom.filter.columns 'favorite_color',orc.dictionary.key.threshold '1.0',orc.column.encoding.direct 'name'
)
列式加密

自 Spark 3.2 起,配合 Apache ORC 1.6 支持 ORC 表的列式加密。以下示例使用指定地址的 Hadoop KMS 作为密钥提供方。详情请参考 Apache Hadoop KMS。

CREATE TABLE encrypted (ssn STRING,email STRING,name STRING
)
USING ORC
OPTIONS (hadoop.security.key.provider.path "kms://http@localhost:9600/kms",orc.key.provider "hadoop",orc.encrypt "pii:ssn,email",orc.mask "nullify:ssn;sha256:email"
)
Hive 元存储 ORC 表转换

当读取或写入 Hive 元存储 ORC 表时,Spark SQL 会尝试使用自身的 ORC 支持(而非 Hive SerDe)以提升性能。对于 CTAS 语句,仅非分区 Hive 元存储 ORC 表会被转换。该行为由 spark.sql.hive.convertMetastoreOrc 配置控制,默认开启。

配置参数
属性名称默认值含义引入版本
spark.sql.orc.implnativeORC 实现名称,可选 nativehive2.3.0
spark.sql.orc.enableVectorizedReadertrue在原生实现中启用向量化 ORC 解码2.3.0
spark.sql.orc.columnarReaderBatchSize4096向量化读取器批处理行数2.4.0
spark.sql.orc.columnarWriterBatchSize1024向量化写入器批处理行数3.4.0
spark.sql.orc.enableNestedColumnVectorizedReadertrue为嵌套数据类型启用向量化解码3.2.0
spark.sql.orc.filterPushdowntrue启用 ORC 文件谓词下推1.4.0
spark.sql.orc.aggregatePushdownfalse启用聚合下推优化3.3.0
spark.sql.orc.mergeSchemafalse是否合并所有数据文件的 Schema3.0.0
spark.sql.hive.convertMetastoreOrctrue是否使用内置 ORC 支持替代 Hive SerDe2.0.0
数据源选项

ORC 数据源选项可通过以下方式设置:

  • DataFrameReader / DataFrameWriter / DataStreamReader / DataStreamWriter.option/.options 方法
  • CREATE TABLE USING DATA_SOURCEOPTIONS 子句
属性名称默认值含义适用范围
mergeSchemafalse是否合并所有 ORC 分片文件的 Schema读取
compressionsnappy保存文件时使用的压缩编解码器写入

其他通用选项请参考通用文件源选项

JSON 文件

Python 方式

Spark SQL 能够自动推断 JSON 数据集的模式并将其加载为 DataFrame。可以使用 SparkSession.read.json 对 JSON 文件进行此转换。

请注意,作为 JSON 文件提供的文件并非典型的 JSON 文件。每行必须包含一个独立的、自包含的有效 JSON 对象。更多信息请参阅 JSON Lines 文本格式(也称为换行符分隔的 JSON)。

对于常规的多行 JSON 文件,请将 multiLine 参数设置为 True

# spark 来自之前的示例。
sc = spark.sparkContext# 路径指向一个 JSON 数据集。
# 路径可以是单个文本文件,也可以是存储文本文件的目录
path = "examples/src/main/resources/people.json"
peopleDF = spark.read.json(path)# 推断的模式可以使用 printSchema() 方法可视化
peopleDF.printSchema()
# root
#  |-- age: long (nullable = true)
#  |-- name: string (nullable = true)# 使用 DataFrame 创建临时视图
peopleDF.createOrReplaceTempView("people")# 可以使用 spark 提供的 sql 方法运行 SQL 语句
teenagerNamesDF = spark.sql("SELECT name FROM people WHERE age BETWEEN 13 AND 19")
teenagerNamesDF.show()
# +------+
# |  name|
# +------+
# |Justin|
# +------+# 或者,可以为由 RDD[String] 表示的 JSON 数据集创建 DataFrame,其中每个字符串存储一个 JSON 对象
jsonStrings = ['{"name":"Yin","address":{"city":"Columbus","state":"Ohio"}}']
otherPeopleRDD = sc.parallelize(jsonStrings)
otherPeople = spark.read.json(otherPeopleRDD)
otherPeople.show()
# +---------------+----+
# |        address|name|
# +---------------+----+
# |[Columbus,Ohio]| Yin|
# +---------------+----+

完整示例代码可在 Spark 代码库的 “examples/src/main/python/sql/datasource.py” 中找到。

Scala 方式

Spark SQL 能够自动推断 JSON 数据集的模式并将其加载为 Dataset[Row]。可以使用 SparkSession.read.json()Dataset[String] 或 JSON 文件进行此转换。

请注意,作为 JSON 文件提供的文件并非典型的 JSON 文件。每行必须包含一个独立的、自包含的有效 JSON 对象。更多信息请参阅 JSON Lines 文本格式(也称为换行符分隔的 JSON)。

对于常规的多行 JSON 文件,请将 multiLine 选项设置为 true

// 原始类型(Int、String等)和 Product 类型(样例类)的编码器在创建 Dataset 时通过导入此包来支持。
import spark.implicits._// 路径指向一个 JSON 数据集。
// 路径可以是单个文本文件,也可以是存储文本文件的目录
val path = "examples/src/main/resources/people.json"
val peopleDF = spark.read.json(path)// 推断的模式可以使用 printSchema() 方法可视化
peopleDF.printSchema()
// root
//  |-- age: long (nullable = true)
//  |-- name: string (nullable = true)// 使用 DataFrame 创建临时视图
peopleDF.createOrReplaceTempView("people")// 可以使用 spark 提供的 sql 方法运行 SQL 语句
val teenagerNamesDF = spark.sql("SELECT name FROM people WHERE age BETWEEN 13 AND 19")
teenagerNamesDF.show()
// +------+
// |  name|
// +------+
// |Justin|
// +------+// 或者,可以为由 Dataset[String] 表示的 JSON 数据集创建 DataFrame,其中每个字符串存储一个 JSON 对象
val otherPeopleDataset = spark.createDataset("""{"name":"Yin","address":{"city":"Columbus","state":"Ohio"}}""" :: Nil)
val otherPeople = spark.read.json(otherPeopleDataset)
otherPeople.show()
// +---------------+----+
// |        address|name|
// +---------------+----+
// |[Columbus,Ohio]| Yin|
// +---------------+----+

完整示例代码可在 Spark 代码库的 “examples/src/main/scala/org/apache/spark/examples/sql/SQLDataSourceExample.scala” 中找到。

Java 方式

Spark SQL 能够自动推断 JSON 数据集的模式并将其加载为 Dataset<Row>。可以使用 SparkSession.read().json()Dataset<String> 或 JSON 文件进行此转换。

请注意,作为 JSON 文件提供的文件并非典型的 JSON 文件。每行必须包含一个独立的、自包含的有效 JSON 对象。更多信息请参阅 JSON Lines 文本格式(也称为换行符分隔的 JSON)。

对于常规的多行 JSON 文件,请将 multiLine 选项设置为 true

import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;// 路径指向一个 JSON 数据集。
// 路径可以是单个文本文件,也可以是存储文本文件的目录
Dataset<Row> people = spark.read().json("examples/src/main/resources/people.json");// 推断的模式可以使用 printSchema() 方法可视化
people.printSchema();
// root
//  |-- age: long (nullable = true)
//  |-- name: string (nullable = true)// 使用 DataFrame 创建临时视图
people.createOrReplaceTempView("people");// 可以使用 spark 提供的 sql 方法运行 SQL 语句
Dataset<Row> namesDF = spark.sql("SELECT name FROM people WHERE age BETWEEN 13 AND 19");
namesDF.show();
// +------+
// |  name|
// +------+
// |Justin|
// +------+// 或者,可以为由 Dataset<String> 表示的 JSON 数据集创建 DataFrame,其中每个字符串存储一个 JSON 对象。
List<String> jsonData = Arrays.asList("{\"name\":\"Yin\",\"address\":{\"city\":\"Columbus\",\"state\":\"Ohio\"}}");
Dataset<String> anotherPeopleDataset = spark.createDataset(jsonData, Encoders.STRING());
Dataset<Row> anotherPeople = spark.read().json(anotherPeopleDataset);
anotherPeople.show();
// +---------------+----+
// |        address|name|
// +---------------+----+
// |[Columbus,Ohio]| Yin|
// +---------------+----+

完整示例代码可在 Spark 代码库的 “examples/src/main/java/org/apache/spark/examples/sql/JavaSQLDataSourceExample.java” 中找到。

R 方式

Spark SQL 能够自动推断 JSON 数据集的模式,并使用 read.json() 函数将其加载为 DataFrame,该函数从 JSON 文件目录加载数据,其中文件的每一行都是一个 JSON 对象。

请注意,作为 JSON 文件提供的文件并非典型的 JSON 文件。每行必须包含一个独立的、自包含的有效 JSON 对象。更多信息请参阅 JSON Lines 文本格式(也称为换行符分隔的 JSON)。

对于常规的多行 JSON 文件,请将命名参数 multiLine 设置为 TRUE

# 路径指向一个 JSON 数据集。
# 路径可以是单个文本文件,也可以是存储文本文件的目录。
path <- "examples/src/main/resources/people.json"
# 从路径指向的文件创建一个 DataFrame
people <- read.json(path)# 推断的模式可以使用 printSchema() 方法可视化。
printSchema(people)
## root
##  |-- age: long (nullable = true)
##  |-- name: string (nullable = true)# 将此 DataFrame 注册为一个表。
createOrReplaceTempView(people, "people")# 可以使用 sql 方法运行 SQL 语句。
teenagers <- sql("SELECT name FROM people WHERE age >= 13 AND age <= 19")
head(teenagers)
##     name
## 1 Justin

完整示例代码可在 Spark 代码库的 “examples/src/main/r/RSparkSQLExample.R” 中找到。

SQL 方式
CREATE TEMPORARY VIEW jsonTable
USING org.apache.spark.sql.json
OPTIONS (path "examples/src/main/resources/people.json"
);SELECT * FROM jsonTable;
数据源选项

JSON 的数据源选项可以通过以下方式设置:

  • DataFrameReader / DataFrameWriter / DataStreamReader / DataStreamWriter.option/.options 方法
  • 以下内置函数:
    • from_json
    • to_json
    • schema_of_json
  • CREATE TABLE USING DATA_SOURCEOPTIONS 子句
属性名称默认值含义适用范围
timeZonespark.sql.session.timeZone 配置的值设置用于格式化 JSON 数据源或分区值中时间戳的时区 ID 字符串。支持的时区格式:
• 基于区域的区域 ID:应具有 ‘area/city’ 形式,如 ‘America/Los_Angeles’。
• 区域偏移量:应为 ‘(+|-)HH:mm’ 格式,例如 ‘-08:00’ 或 ‘+01:00’。‘UTC’ 和 ‘Z’ 也作为 ‘+00:00’ 的别名被支持。
不推荐使用像 ‘CST’ 这样的其他短名称,因为它们可能不明确。
读/写
primitivesAsStringfalse将所有原始值推断为字符串类型。读取
prefersDecimalfalse将所有浮点值推断为小数类型。如果值不适合小数,则推断为双精度类型。读取
allowCommentsfalse忽略 JSON 记录中的 Java/C++ 风格注释。读取
allowUnquotedFieldNamesfalse允许未加引号的 JSON 字段名称。读取
allowSingleQuotestrue除双引号外,还允许单引号。读取
allowNumericLeadingZerosfalse允许数字中的前导零(例如 00012)。读取
allowBackslashEscapingAnyCharacterfalse允许使用反斜杠引用机制接受所有字符的引用。读取
modePERMISSIVE允许在解析过程中处理损坏记录的模式:
PERMISSIVE:遇到损坏记录时,将格式错误的字符串放入由 columnNameOfCorruptRecord 配置的字段中,并将格式错误的字段设置为 null。要保留损坏记录,用户可以在用户定义的模式中设置一个名为 columnNameOfCorruptRecord 的字符串类型字段。如果模式中没有该字段,则在解析期间删除损坏记录。推断模式时,它会在输出模式中隐式添加一个 columnNameOfCorruptRecord 字段。
DROPMALFORMED:忽略整个损坏的记录。JSON 内置函数不支持此模式。
FAILFAST:遇到损坏记录时抛出异常。
读取
columnNameOfCorruptRecordspark.sql.columnNameOfCorruptRecord 配置的值允许重命名由 PERMISSIVE 模式创建的包含格式错误字符串的新字段。这会覆盖 spark.sql.columnNameOfCorruptRecord读取
dateFormatyyyy-MM-dd设置指示日期格式的字符串。自定义日期格式遵循日期时间模式中的格式。适用于日期类型。读/写
timestampFormatyyyy-MM-dd'T'HH:mm:ss[.SSS][XXX]设置指示时间戳格式的字符串。自定义日期格式遵循日期时间模式中的格式。适用于时间戳类型。读/写
timestampNTZFormatyyyy-MM-dd'T'HH:mm:ss[.SSS]设置指示无时区时间戳格式的字符串。自定义日期格式遵循日期时间模式中的格式。适用于无时区时间戳类型,请注意在读取或写入此数据类型时不支持区域偏移量和时区组件。读/写
enableDateTimeParsingFallback如果时间解析策略具有旧版设置或未提供自定义日期或时间戳模式,则启用。如果值不匹配设置的模式,允许回退到向后兼容(Spark 1.x 和 2.0)的日期和时间戳解析行为。读取
multiLinefalse每个文件解析一条可能跨多行的记录。JSON 内置函数忽略此选项。读取
allowUnquotedControlCharsfalse允许或不允许 JSON 字符串包含未加引号的控制字符(ASCII 值小于 32 的字符,包括制表符和换行符)。读取
encodingmultiLine 设置为 true 时自动检测(用于读取),UTF-8(用于写入)对于读取,允许为 JSON 文件强制设置一种标准基本或扩展编码。例如 UTF-16BE、UTF-32LE。对于写入,指定保存的 json 文件的编码(字符集)。JSON 内置函数忽略此选项。读/写
lineSep\r, \r\n, \n(用于读取),\n(用于写入)定义应用于解析的行分隔符。JSON 内置函数忽略此选项。读/写
samplingRatio1.0定义用于模式推断的输入 JSON 对象的比例。读取
dropFieldIfAllNullfalse在模式推断期间是否忽略全为 null 值或空数组的列。读取
localeen-US设置一个区域设置作为 IETF BCP 47 格式的语言标签。例如,在解析日期和时间戳时使用区域设置。读取
allowNonNumericNumberstrue允许 JSON 解析器识别一组“非数字”(NaN)标记作为合法的浮点数值:
+INF:表示正无穷,以及 +InfinityInfinity 的别名。
-INF:表示负无穷,别名 -Infinity
NaN:表示其他非数字,如除零的结果。
读取
compression(none)保存到文件时使用的压缩编解码器。这可以是已知的不区分大小写的短名称之一(none、bzip2、gzip、lz4、snappy 和 deflate)。JSON 内置函数忽略此选项。写入
ignoreNullFieldsspark.sql.jsonGenerator.ignoreNullFields 配置的值生成 JSON 对象时是否忽略 null 字段。写入

其他通用选项请参考通用文件源选项

CSV 文件

Spark SQL 提供 spark.read().csv("file_name") 来读取 CSV 格式的文件或文件目录到 Spark DataFrame,并使用 dataframe.write().csv("path") 写入 CSV 文件。可以使用 option() 函数来自定义读取或写入的行为,例如控制表头、分隔符、字符集等。

# spark 来自之前的示例
sc = spark.sparkContext# 路径指向一个 CSV 数据集。
# 路径可以是单个 CSV 文件,也可以是 CSV 文件的目录
path = "examples/src/main/resources/people.csv"df = spark.read.csv(path)
df.show()
# +------------------+
# |               _c0|
# +------------------+
# |      name;age;job|
# |Jorge;30;Developer|
# |  Bob;32;Developer|
# +------------------+# 使用分隔符读取 csv,默认分隔符是 ","
df2 = spark.read.option("delimiter", ";").csv(path)
df2.show()
# +-----+---+---------+
# |  _c0|_c1|      _c2|
# +-----+---+---------+
# | name|age|      job|
# |Jorge| 30|Developer|
# |  Bob| 32|Developer|
# +-----+---+---------+# 使用分隔符和表头读取 csv
df3 = spark.read.option("delimiter", ";").option("header", True).csv(path)
df3.show()
# +-----+---+---------+
# | name|age|      job|
# +-----+---+---------+
# |Jorge| 30|Developer|
# |  Bob| 32|Developer|
# +-----+---+---------+# 你也可以使用 options() 来设置多个选项
df4 = spark.read.options(delimiter=";", header=True).csv(path)# "output" 是一个包含多个 csv 文件和一个 _SUCCESS 文件的文件夹。
df3.write.csv("output")# 读取文件夹中的所有文件,请确保文件夹中只应存在 CSV 文件。
folderPath = "examples/src/main/resources"
df5 = spark.read.csv(folderPath)
df5.show()
# 错误的模式,因为读取了非 CSV 文件
# +-----------+
# |        _c0|
# +-----------+
# |238val_238|
# |  86val_86|
# |311val_311|
# |  27val_27|
# |165val_165|
# +-----------+

完整示例代码可在 Spark 代码库的 “examples/src/main/python/sql/datasource.py” 中找到。

// 路径指向一个 CSV 数据集。
// 路径可以是单个 CSV 文件,也可以是 CSV 文件的目录
val path = "examples/src/main/resources/people.csv"val df = spark.read.csv(path)
df.show()
// +------------------+
// |               _c0|
// +------------------+
// |      name;age;job|
// |Jorge;30;Developer|
// |  Bob;32;Developer|
// +------------------+// 使用分隔符读取 csv,默认分隔符是 ","
val df2 = spark.read.option("delimiter", ";").csv(path)
df2.show()
// +-----+---+---------+
// |  _c0|_c1|      _c2|
// +-----+---+---------+
// | name|age|      job|
// |Jorge| 30|Developer|
// |  Bob| 32|Developer|
// +-----+---+---------+// 使用分隔符和表头读取 csv
val df3 = spark.read.option("delimiter", ";").option("header", "true").csv(path)
df3.show()
// +-----+---+---------+
// | name|age|      job|
// +-----+---+---------+
// |Jorge| 30|Developer|
// |  Bob| 32|Developer|
// +-----+---+---------+// 你也可以使用 options() 来设置多个选项
val df4 = spark.read.options(Map("delimiter"->";", "header"->"true")).csv(path)// "output" 是一个包含多个 csv 文件和一个 _SUCCESS 文件的文件夹。
df3.write.csv("output")// 读取文件夹中的所有文件,请确保文件夹中只应存在 CSV 文件。
val folderPath = "examples/src/main/resources";
val df5 = spark.read.csv(folderPath);
df5.show();
// 错误的模式,因为读取了非 CSV 文件
// +-----------+
// |        _c0|
// +-----------+
// |238val_238|
// |  86val_86|
// |311val_311|
// |  27val_27|
// |165val_165|
// +-----------+

完整示例代码可在 Spark 代码库的 “examples/src/main/scala/org/apache/spark/examples/sql/SQLDataSourceExample.scala” 中找到。

import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;// 路径指向一个 CSV 数据集。
// 路径可以是单个 CSV 文件,也可以是 CSV 文件的目录
String path = "examples/src/main/resources/people.csv";Dataset<Row> df = spark.read().csv(path);
df.show();
// +------------------+
// |               _c0|
// +------------------+
// |      name;age;job|
// |Jorge;30;Developer|
// |  Bob;32;Developer|
// +------------------+// 使用分隔符读取 csv,默认分隔符是 ","
Dataset<Row> df2 = spark.read().option("delimiter", ";").csv(path);
df2.show()
// +-----+---+---------+
// |  _c0|_c1|      _c2|
// +-----+---+---------+
// | name|age|      job|
// |Jorge| 30|Developer|
// |  Bob| 32|Developer|
// +-----+---+---------+// 使用分隔符和表头读取 csv
Dataset<Row> df3 = spark.read().option("delimiter", ";").option("header", "true").csv(path);
df3.show()
// +-----+---+---------+
// | name|age|      job|
// +-----+---+---------+
// |Jorge| 30|Developer|
// |  Bob| 32|Developer|
// +-----+---+---------+// 你也可以使用 options() 来设置多个选项
java.util.Map<String, String> optionsMap = new java.util.HashMap<String, String>();
optionsMap.put("delimiter",";");
optionsMap.put("header","true");
Dataset<Row> df4 = spark.read().options(optionsMap).csv(path);// "output" 是一个包含多个 csv 文件和一个 _SUCCESS 文件的文件夹。
df3.write().csv("output");// 读取文件夹中的所有文件,请确保文件夹中只应存在 CSV 文件。
String folderPath = "examples/src/main/resources";
Dataset<Row> df5 = spark.read().csv(folderPath);
df5.show();
// 错误的模式,因为读取了非 CSV 文件
// +-----------+
// |        _c0|
// +-----------+
// |238val_238|
// |  86val_86|
// |311val_311|
// |  27val_27|
// |165val_165|
// +-----------+

完整示例代码可在 Spark 代码库的 “examples/src/main/java/org/apache/spark/examples/sql/JavaSQLDataSourceExample.java” 中找到。

数据源选项

CSV 的数据源选项可以通过以下方式设置:

  • DataFrameReader / DataFrameWriter / DataStreamReader / DataStreamWriter.option/.options 方法
  • 以下内置函数:
    • from_csv
    • to_csv
    • schema_of_csv
  • CREATE TABLE USING DATA_SOURCEOPTIONS 子句
属性名称默认值含义适用范围
sep,为每个字段和值设置分隔符。此分隔符可以是一个或多个字符。读/写
encodingUTF-8对于读取,按给定的编码类型解码 CSV 文件。对于写入,指定保存的 CSV 文件的编码(字符集)。CSV 内置函数忽略此选项。读/写
quote"设置用于转义引号值的单个字符,其中分隔符可能是值的一部分。对于读取,如果要关闭引号,需要设置为非空但为空字符串。对于写入,如果设置了空字符串,则使用 \u0000(空字符)。读/写
quoteAllfalse指示是否所有值都应始终用引号括起来的标志。默认仅转义包含引号字符的值。写入
escape\设置用于在已引用的值内转义引号的单个字符。读/写
escapeQuotestrue指示包含引号的值是否应始终用引号括起来的标志。默认转义所有包含引号字符的值。写入
comment设置用于跳过以此字符开头的行的单个字符。默认情况下禁用。读取
headerfalse对于读取,使用第一行作为列名。对于写入,将列名作为第一行写入。请注意,如果给定路径是字符串 RDD,则此 header 选项将删除所有与表头相同的行(如果存在)。CSV 内置函数忽略此选项。读/写
inferSchemafalse自动从数据推断输入模式。这需要对数据进行一次额外遍历。CSV 内置函数忽略此选项。读取
preferDatetrue在模式推断(inferSchema)期间,如果值满足 dateFormat 选项或默认日期格式,尝试将包含日期的字符串列推断为 Date 类型。对于包含日期和时间戳混合的列,如果未指定时间戳格式,则尝试将它们推断为 TimestampType,否则推断为 StringType读取
enforceSchematrue如果设置为 true,则指定的或推断的模式将被强制应用于数据源文件,并且 CSV 文件中的表头将被忽略。如果选项设置为 false,当 header 选项设置为 true 时,将根据 CSV 文件中的所有表头验证模式。模式中的字段名称和 CSV 表头中的列名称会根据其位置进行检查,同时考虑 spark.sql.caseSensitive。尽管默认值为 true,但建议禁用 enforceSchema 选项以避免不正确的结果。CSV 内置函数忽略此选项。读取
ignoreLeadingWhiteSpacefalse (用于读取), true (用于写入)指示是否应跳过正在读取/写入的值中的前导空白字符的标志。读/写
ignoreTrailingWhiteSpacefalse (用于读取), true (用于写入)指示是否应跳过正在读取/写入的值中的尾随空白字符的标志。读/写
nullValue设置 null 值的字符串表示形式。自 2.0.1 起,此 nullValue 参数适用于所有支持的类型,包括字符串类型。读/写
nanValueNaN设置非数字值的字符串表示形式。读取
positiveInfInf设置正无穷大值的字符串表示形式。读取
negativeInf-Inf设置负无穷大值的字符串表示形式。读取
dateFormatyyyy-MM-dd设置指示日期格式的字符串。自定义日期格式遵循 Datetime Patterns 中的格式。适用于日期类型。读/写
timestampFormatyyyy-MM-dd'T'HH:mm:ss[.SSS][XXX]设置指示时间戳格式的字符串。自定义日期格式遵循 Datetime Patterns 中的格式。适用于时间戳类型。读/写
timestampNTZFormatyyyy-MM-dd'T'HH:mm:ss[.SSS]设置指示无时区时间戳格式的字符串。自定义日期格式遵循 Datetime Patterns 中的格式。适用于无时区时间戳类型,请注意在读取或写入此数据类型时不支持区域偏移量和时区组件。读/写
enableDateTimeParsingFallback如果时间解析策略具有旧版设置或未提供自定义日期或时间戳模式,则启用。如果值不匹配设置的模式,允许回退到向后兼容(Spark 1.x 和 2.0)的日期和时间戳解析行为。读取
maxColumns20480定义一条记录可以拥有的最大列数的硬限制。读取
maxCharsPerColumn-1定义正在读取的任何给定值允许的最大字符数。默认情况下为 -1,表示长度不受限制。读取
modePERMISSIVE允许在解析过程中处理损坏记录的模式。它支持以下不区分大小写的模式。请注意,Spark 在列裁剪下仅尝试解析 CSV 中所需的列。因此,损坏的记录可能因所需的字段集而异。此行为可由 spark.sql.csv.parser.columnPruning.enabled(默认启用)控制。
PERMISSIVE:遇到损坏记录时,将格式错误的字符串放入由 columnNameOfCorruptRecord 配置的字段中,并将格式错误的字段设置为 null。要保留损坏记录,用户可以在用户定义的模式中设置一个名为 columnNameOfCorruptRecord 的字符串类型字段。如果模式中没有该字段,则在解析期间删除损坏记录。标记比模式少/多的记录对 CSV 来说不是损坏记录。当遇到标记比模式长度少的记录时,将额外字段设置为 null。当记录的标记比模式长度多时,它会丢弃额外的标记。
DROPMALFORMED:忽略整个损坏的记录。CSV 内置函数不支持此模式。
FAILFAST:遇到损坏记录时抛出异常。
读取
columnNameOfCorruptRecordspark.sql.columnNameOfCorruptRecord 配置的值允许重命名由 PERMISSIVE 模式创建的包含格式错误字符串的新字段。这会覆盖 spark.sql.columnNameOfCorruptRecord读取
multiLinefalse每个文件解析一条可能跨多行的记录。CSV 内置函数忽略此选项。读取
charToEscapeQuoteEscapingescape\0设置用于转义引号字符的转义符的单个字符。当转义字符和引号字符不同时,默认值为转义字符,否则为 \0读/写
samplingRatio1.0定义用于模式推断的行比例。CSV 内置函数忽略此选项。读取
emptyValue(用于读取), "" (用于写入)设置空值的字符串表示形式。读/写
localeen-US设置一个区域设置作为 IETF BCP 47 格式的语言标签。例如,这在解析日期和时间戳时使用。读取
lineSep\r, \r\n\n (用于读取), \n (用于写入)定义应用于解析/写入的行分隔符。最大长度为 1 个字符。CSV 内置函数忽略此选项。读/写
unescapedQuoteHandlingSTOP_AT_DELIMITER定义 CsvParser 将如何处理包含未转义引号的值:
STOP_AT_CLOSING_QUOTE:如果在输入中找到未转义的引号,则累积引号字符并继续将该值作为带引号的值进行解析,直到找到结束引号。
BACK_TO_DELIMITER:如果在输入中找到未转义的引号,则将该值视为未加引号的值。这将使解析器累积当前解析值的所有字符,直到找到分隔符。如果在值中未找到分隔符,解析器将继续从输入中累积字符,直到找到分隔符或行结束符。
STOP_AT_DELIMITER:如果在输入中找到未转义的引号,则将该值视为未加引号的值。这将使解析器累积所有字符,直到在输入中找到分隔符或行结束符。
SKIP_VALUE:如果在输入中找到未转义的引号,将跳过为该给定值解析的内容,并生成在 nullValue 中设置的值。
RAISE_ERROR:如果在输入中找到未转义的引号,将抛出 TextParsingException。
读取
compression(none)保存到文件时使用的压缩编解码器。这可以是已知的不区分大小写的短名称之一(none、bzip2、gzip、lz4、snappy 和 deflate)。CSV 内置函数忽略此选项。写入

其他通用选项请参考通用文件源选项

Txt 文件

Spark SQL 提供 spark.read().text("file_name") 来读取文本文件或文本文件目录到 Spark DataFrame,并使用 dataframe.write().text("path") 写入文本文件。读取文本文件时,默认情况下每一行成为具有字符串 “value” 列的每一行。如下例所示,可以更改行分隔符。可以使用 option() 函数来自定义读取或写入的行为,例如控制行分隔符、压缩等。

# spark 来自之前的示例
sc = spark.sparkContext# 路径指向一个文本数据集。
# 路径可以是单个文本文件,也可以是文本文件的目录
path = "examples/src/main/resources/people.txt"df1 = spark.read.text(path)
df1.show()
# +-----------+
# |      value|
# +-----------+
# |Michael, 29|
# |   Andy, 30|
# | Justin, 19|
# +-----------+# 您可以使用 'lineSep' 选项来定义行分隔符。
# 默认情况下,行分隔符处理所有 `\r`、`\r\n` 和 `\n`。
df2 = spark.read.text(path, lineSep=",")
df2.show()
# +-----------+
# |      value|
# +-----------+
# |    Michael|
# |   29\nAndy|
# | 30\nJustin|
# |       19\n|
# +-----------+# 您也可以使用 'wholetext' 选项将每个输入文件作为单行读取。
df3 = spark.read.text(path, wholetext=True)
df3.show()
# +--------------------+
# |               value|
# +--------------------+
# |Michael, 29\nAndy...|
# +--------------------+# "output" 是一个包含多个文本文件和一个 _SUCCESS 文件的文件夹。
df1.write.csv("output")# 您可以使用 'compression' 选项指定压缩格式。
df1.write.text("output_compressed", compression="gzip")

完整示例代码可在 Spark 代码库的 “examples/src/main/python/sql/datasource.py” 中找到。

// 路径指向一个文本数据集。
// 路径可以是单个文本文件,也可以是文本文件的目录
val path = "examples/src/main/resources/people.txt"val df1 = spark.read.text(path)
df1.show()
// +-----------+
// |      value|
// +-----------+
// |Michael, 29|
// |   Andy, 30|
// | Justin, 19|
// +-----------+// 您可以使用 'lineSep' 选项来定义行分隔符。
// 默认情况下,行分隔符处理所有 `\r`、`\r\n` 和 `\n`。
val df2 = spark.read.option("lineSep", ",").text(path)
df2.show()
// +-----------+
// |      value|
// +-----------+
// |    Michael|
// |   29\nAndy|
// | 30\nJustin|
// |       19\n|
// +-----------+// 您也可以使用 'wholetext' 选项将每个输入文件作为单行读取。
val df3 = spark.read.option("wholetext", true).text(path)
df3.show()
//  +--------------------+
//  |               value|
//  +--------------------+
//  |Michael, 29\nAndy...|
//  +--------------------+// "output" 是一个包含多个文本文件和一个 _SUCCESS 文件的文件夹。
df1.write.text("output")// 您可以使用 'compression' 选项指定压缩格式。
df1.write.option("compression", "gzip").text("output_compressed")

完整示例代码可在 Spark 代码库的 “examples/src/main/scala/org/apache/spark/examples/sql/SQLDataSourceExample.scala” 中找到。

import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;// 路径指向一个文本数据集。
// 路径可以是单个文本文件,也可以是文本文件的目录
String path = "examples/src/main/resources/people.txt";Dataset<Row> df1 = spark.read().text(path);
df1.show();
// +-----------+
// |      value|
// +-----------+
// |Michael, 29|
// |   Andy, 30|
// | Justin, 19|
// +-----------+// 您可以使用 'lineSep' 选项来定义行分隔符。
// 默认情况下,行分隔符处理所有 `\r`、`\r\n` 和 `\n`。
Dataset<Row> df2 = spark.read().option("lineSep", ",").text(path);
df2.show();
// +-----------+
// |      value|
// +-----------+
// |    Michael|
// |   29\nAndy|
// | 30\nJustin|
// |       19\n|
// +-----------+// 您也可以使用 'wholetext' 选项将每个输入文件作为单行读取。
Dataset<Row> df3 = spark.read().option("wholetext", "true").text(path);
df3.show();
//  +--------------------+
//  |               value|
//  +--------------------+
//  |Michael, 29\nAndy...|
//  +--------------------+// "output" 是一个包含多个文本文件和一个 _SUCCESS 文件的文件夹。
df1.write().text("output");// 您可以使用 'compression' 选项指定压缩格式。
df1.write().option("compression", "gzip").text("output_compressed");

完整示例代码可在 Spark 代码库的 “examples/src/main/java/org/apache/spark/examples/sql/JavaSQLDataSourceExample.java” 中找到。

数据源选项

文本的数据源选项可以通过以下方式设置:

  • DataFrameReader / DataFrameWriter / DataStreamReader / DataStreamWriter.option/.options 方法
  • CREATE TABLE USING DATA_SOURCEOPTIONS 子句
属性名称默认值含义适用范围
wholetextfalse如果为 true,则将输入路径中的每个文件作为单行读取。读取
lineSep\r, \r\n, \n (用于读取), \n (用于写入)定义应用于读取或写入的行分隔符。读/写
compression(none)保存到文件时使用的压缩编解码器。这可以是已知的不区分大小写的短名称之一(none、bzip2、gzip、lz4、snappy 和 deflate)。写入

其他通用选项请参考通用文件源选项

Hive 表

Spark SQL 也支持读写存储在 Apache Hive 中的数据。然而,由于 Hive 有大量的依赖项,这些依赖项并未包含在默认的 Spark 发行版中。如果在类路径上可以找到 Hive 依赖项,Spark 将自动加载它们。请注意,这些 Hive 依赖项也必须存在于所有工作节点上,因为它们需要访问 Hive 序列化和反序列化库(SerDes)才能访问存储在 Hive 中的数据。

Hive 的配置是通过将您的 hive-site.xmlcore-site.xml(用于安全配置)和 hdfs-site.xml(用于 HDFS 配置)文件放置在 conf/ 目录下完成的。

当使用 Hive 时,必须实例化具有 Hive 支持的 SparkSession,包括连接到持久化的 Hive 元存储、支持 Hive SerDes 以及 Hive 用户定义函数。没有现有 Hive 部署的用户仍然可以启用 Hive 支持。当未通过 hive-site.xml 配置时,上下文会自动在当前目录中创建 metastore_db,并创建一个由 spark.sql.warehouse.dir 配置的目录,该目录默认为启动 Spark 应用程序的当前目录中的 spark-warehouse 目录。请注意,自 Spark 2.0.0 起,hive-site.xml 中的 hive.metastore.warehouse.dir 属性已被弃用。请改用 spark.sql.warehouse.dir 来指定仓库中数据库的默认位置。您可能需要向启动 Spark 应用程序的用户授予写权限。

from os.path import abspathfrom pyspark.sql import SparkSession
from pyspark.sql import Row# warehouse_location 指向托管数据库和表的默认位置
warehouse_location = abspath('spark-warehouse')spark = SparkSession \.builder \.appName("Python Spark SQL Hive integration example") \.config("spark.sql.warehouse.dir", warehouse_location) \.enableHiveSupport() \.getOrCreate()# spark 是一个已存在的 SparkSession
spark.sql("CREATE TABLE IF NOT EXISTS src (key INT, value STRING) USING hive")
spark.sql("LOAD DATA LOCAL INPATH 'examples/src/main/resources/kv1.txt' INTO TABLE src")# 查询使用 HiveQL 表达
spark.sql("SELECT * FROM src").show()
# +---+-------+
# |key|  value|
# +---+-------+
# |238|val_238|
# | 86| val_86|
# |311|val_311|
# ...# 也支持聚合查询。
spark.sql("SELECT COUNT(*) FROM src").show()
# +--------+
# |count(1)|
# +--------+
# |    500 |
# +--------+# SQL 查询的结果本身是 DataFrame,并支持所有常规函数。
sqlDF = spark.sql("SELECT key, value FROM src WHERE key < 10 ORDER BY key")# DataFrame 中的项是 Row 类型,允许您按序号访问每一列。
stringsDS = sqlDF.rdd.map(lambda row: "Key: %d, Value: %s" % (row.key, row.value))
for record in stringsDS.collect():print(record)
# Key: 0, Value: val_0
# Key: 0, Value: val_0
# Key: 0, Value: val_0
# ...# 您也可以使用 DataFrame 在 SparkSession 内创建临时视图。
Record = Row("key", "value")
recordsDF = spark.createDataFrame([Record(i, "val_" + str(i)) for i in range(1, 101)])
recordsDF.createOrReplaceTempView("records")# 查询随后可以将 DataFrame 数据与存储在 Hive 中的数据连接起来。
spark.sql("SELECT * FROM records r JOIN src s ON r.key = s.key").show()
# +---+------+---+------+
# |key| value|key| value|
# +---+------+---+------+
# |  2| val_2|  2| val_2|
# |  4| val_4|  4| val_4|
# |  5| val_5|  5| val_5|
# ...

完整示例代码可在 Spark 代码库的 “examples/src/main/python/sql/hive.py” 中找到。

import java.io.Fileimport org.apache.spark.sql.{Row, SaveMode, SparkSession}case class Record(key: Int, value: String)// warehouseLocation 指向托管数据库和表的默认位置
val warehouseLocation = new File("spark-warehouse").getAbsolutePathval spark = SparkSession.builder().appName("Spark Hive Example").config("spark.sql.warehouse.dir", warehouseLocation).enableHiveSupport().getOrCreate()import spark.implicits._
import spark.sqlsql("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")// 查询使用 HiveQL 表达
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 |
// +--------+// SQL 查询的结果本身是 DataFrame,并支持所有常规函数。
val sqlDF = sql("SELECT key, value FROM src WHERE key < 10 ORDER BY key")// DataFrame 中的项是 Row 类型,允许您按序号访问每一列。
val stringsDS = sqlDF.map {case Row(key: Int, value: String) => s"Key: $key, Value: $value"
}
stringsDS.show()
// +--------------------+
// |               value|
// +--------------------+
// |Key: 0, Value: val_0|
// |Key: 0, Value: val_0|
// |Key: 0, Value: val_0|
// ...// 您也可以使用 DataFrame 在 SparkSession 内创建临时视图。
val recordsDF = spark.createDataFrame((1 to 100).map(i => Record(i, s"val_$i")))
recordsDF.createOrReplaceTempView("records")// 查询随后可以将 DataFrame 数据与存储在 Hive 中的数据连接起来。
sql("SELECT * FROM records r JOIN src s ON r.key = s.key").show()
// +---+------+---+------+
// |key| value|key| value|
// +---+------+---+------+
// |  2| val_2|  2| val_2|
// |  4| val_4|  4| val_4|
// |  5| val_5|  5| val_5|
// ...// 创建一个 Hive 托管的 Parquet 表,使用 HQL 语法而非 Spark SQL 原生语法 `USING hive`
sql("CREATE TABLE hive_records(key int, value string) STORED AS PARQUET")
// 将 DataFrame 保存到 Hive 托管表
val df = spark.table("src")
df.write.mode(SaveMode.Overwrite).saveAsTable("hive_records")
// 插入后,Hive 托管表现在有数据了
sql("SELECT * FROM hive_records").show()
// +---+-------+
// |key|  value|
// +---+-------+
// |238|val_238|
// | 86| val_86|
// |311|val_311|
// ...// 准备一个 Parquet 数据目录
val dataDir = "/tmp/parquet_data"
spark.range(10).write.parquet(dataDir)
// 创建一个 Hive 外部 Parquet 表
sql(s"CREATE EXTERNAL TABLE hive_bigints(id bigint) STORED AS PARQUET LOCATION '$dataDir'")
// Hive 外部表应该已经有数据了
sql("SELECT * FROM hive_bigints").show()
// +---+
// | id|
// +---+
// |  0|
// |  1|
// |  2|
// ... 顺序可能不同,因为 spark 并行处理分区。// 开启 Hive 动态分区标志
spark.sqlContext.setConf("hive.exec.dynamic.partition", "true")
spark.sqlContext.setConf("hive.exec.dynamic.partition.mode", "nonstrict")
// 使用 DataFrame API 创建一个 Hive 分区表
df.write.partitionBy("key").format("hive").saveAsTable("hive_part_tbl")
// 分区列 `key` 将被移动到 schema 的末尾。
sql("SELECT * FROM hive_part_tbl").show()
// +-------+---+
// |  value|key|
// +-------+---+
// |val_238|238|
// | val_86| 86|
// |val_311|311|
// ...spark.stop()

完整示例代码可在 Spark 代码库的 “examples/src/main/scala/org/apache/spark/examples/sql/hive/SparkHiveExample.scala” 中找到。

import java.io.File;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;import org.apache.spark.api.java.function.MapFunction;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Encoders;
import org.apache.spark.sql.Row;
import org.apache.spark.sql.SparkSession;public static class Record implements Serializable {private int key;private String value;public int getKey() {return key;}public void setKey(int key) {this.key = key;}public String getValue() {return value;}public void setValue(String value) {this.value = value;}
}// warehouseLocation 指向托管数据库和表的默认位置
String warehouseLocation = new File("spark-warehouse").getAbsolutePath();
SparkSession spark = SparkSession.builder().appName("Java Spark Hive Example").config("spark.sql.warehouse.dir", warehouseLocation).enableHiveSupport().getOrCreate();spark.sql("CREATE TABLE IF NOT EXISTS src (key INT, value STRING) USING hive");
spark.sql("LOAD DATA LOCAL INPATH 'examples/src/main/resources/kv1.txt' INTO TABLE src");// 查询使用 HiveQL 表达
spark.sql("SELECT * FROM src").show();
// +---+-------+
// |key|  value|
// +---+-------+
// |238|val_238|
// | 86| val_86|
// |311|val_311|
// ...// 也支持聚合查询。
spark.sql("SELECT COUNT(*) FROM src").show();
// +--------+
// |count(1)|
// +--------+
// |    500 |
// +--------+// SQL 查询的结果本身是 DataFrame,并支持所有常规函数。
Dataset<Row> sqlDF = spark.sql("SELECT key, value FROM src WHERE key < 10 ORDER BY key");// DataFrame 中的项是 Row 类型,允许您按序号访问每一列。
Dataset<String> stringsDS = sqlDF.map((MapFunction<Row, String>) row -> "Key: " + row.get(0) + ", Value: " + row.get(1),Encoders.STRING());
stringsDS.show();
// +--------------------+
// |               value|
// +--------------------+
// |Key: 0, Value: val_0|
// |Key: 0, Value: val_0|
// |Key: 0, Value: val_0|
// ...// 您也可以使用 DataFrame 在 SparkSession 内创建临时视图。
List<Record> records = new ArrayList<>();
for (int key = 1; key < 100; key++) {Record record = new Record();record.setKey(key);record.setValue("val_" + key);records.add(record);
}
Dataset<Row> recordsDF = spark.createDataFrame(records, Record.class);
recordsDF.createOrReplaceTempView("records");// 查询随后可以将 DataFrame 数据与存储在 Hive 中的数据连接起来。
spark.sql("SELECT * FROM records r JOIN src s ON r.key = s.key").show();
// +---+------+---+------+
// |key| value|key| value|
// +---+------+---+------+
// |  2| val_2|  2| val_2|
// |  2| val_2|  2| val_2|
// |  4| val_4|  4| val_4|
// ...

完整示例代码可在 Spark 代码库的 “examples/src/main/java/org/apache/spark/examples/sql/hive/JavaSparkHiveExample.java” 中找到。

当使用 Hive 时,必须实例化具有 Hive 支持的 SparkSession。这增加了在元存储中查找表和使用 HiveQL 编写查询的支持。

# enableHiveSupport 默认为 TRUE
sparkR.session(enableHiveSupport = TRUE)
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")# 查询可以使用 HiveQL 表达。
results <- collect(sql("FROM src SELECT key, value"))

完整示例代码可在 Spark 代码库的 “examples/src/main/r/RSparkSQLExample.R” 中找到。

指定 Hive 表的存储格式

当您创建 Hive 表时,您需要定义该表应如何从文件系统读取/写入数据,即“输入格式”和“输出格式”。您还需要定义该表应如何将数据反序列化为行,或将行序列化为数据,即“serde”。以下选项可用于指定存储格式(“serde”、“input format”、“output format”),例如 CREATE TABLE src(id int) USING hive OPTIONS(fileFormat 'parquet')。默认情况下,我们将以纯文本形式读取表文件。请注意,创建表时还不支持 Hive 存储处理程序,您可以在 Hive 端使用存储处理程序创建表,然后使用 Spark SQL 读取它。

属性名称含义
fileFormatfileFormat 是一种存储格式规范的包,包括 “serde”、“input format” 和 “output format”。目前我们支持 6 种 fileFormat:‘sequencefile’, ‘rcfile’, ‘orc’, ‘parquet’, ‘textfile’ 和 ‘avro’。
inputFormat, outputFormat这两个选项以字符串字面量的形式指定相应的 InputFormat 和 OutputFormat 类的名称,例如 org.apache.hadoop.hive.ql.io.orc.OrcInputFormat。这两个选项必须成对出现,并且如果您已经指定了 fileFormat 选项,则不能指定它们。
serde此选项指定 serde 类的名称。当指定了 fileFormat 选项时,如果给定的 fileFormat 已经包含 serde 信息,则不要指定此选项。目前 “sequencefile”、“textfile” 和 “rcfile” 不包含 serde 信息,您可以将此选项与这 3 种 fileFormat 一起使用。
fieldDelim, escapeDelim, collectionDelim, mapkeyDelim, lineDelim这些选项只能与 “textfile” fileFormat 一起使用。它们定义了如何将分隔文件读取为行。

所有其他使用 OPTIONS 定义的属性将被视为 Hive serde 属性。

与不同版本的 Hive 元存储交互

Spark SQL 的 Hive 支持最重要的部分之一是与 Hive 元存储的交互,这使得 Spark SQL 能够访问 Hive 表的元数据。从 Spark 1.4.0 开始,可以使用下面描述的配置,使用 Spark SQL 的单个二进制构建来查询不同版本的 Hive 元存储。请注意,无论用于与元存储通信的 Hive 版本是什么,Spark SQL 内部都将针对内置的 Hive 进行编译,并使用这些类进行内部执行(serdes、UDFs、UDAFs 等)。

以下选项可用于配置用于检索元数据的 Hive 版本:

属性名称默认值含义引入版本
spark.sql.hive.metastore.version2.3.9Hive 元存储的版本。可用选项包括 0.12.0 到 2.3.9 以及 3.0.0 到 3.1.3。1.4.0
spark.sql.hive.metastore.jarsbuiltin应用于实例化 HiveMetastoreClient 的 jar 包的位置。此属性可以是以下四个选项之一:
builtin:使用 Hive 2.3.9,当启用 -Phive 时,它与 Spark 程序集捆绑在一起。选择此选项时,spark.sql.hive.metastore.version 必须为 2.3.9 或未定义。
maven:使用从 Maven 仓库下载的指定版本的 Hive jars。此配置通常不推荐用于生产部署。
path:使用由 spark.sql.hive.metastore.jars.path 以逗号分隔格式配置的 Hive jars。支持本地或远程路径。提供的 jars 应与 spark.sql.hive.metastore.version 版本相同。
• JVM 标准格式的类路径。此类路径必须包含所有 Hive 及其依赖项,包括正确版本的 Hadoop。提供的 jars 应与 spark.sql.hive.metastore.version 版本相同。这些 jars 只需要存在于驱动程序上,但如果您在 yarn 集群模式下运行,则必须确保它们与您的应用程序一起打包。
1.4.0
spark.sql.hive.metastore.jars.path(empty)用于实例化 HiveMetastoreClient 的 jar 包的逗号分隔路径。此配置仅在 spark.sql.hive.metastore.jars 设置为 path 时有用。
路径可以是以下任何格式:
file://path/to/jar/foo.jar
hdfs://nameservice/path/to/jar/foo.jar
/path/to/jar/(无 URI 方案的路径遵循 conf fs.defaultFS 的 URI 模式)
[http/https/ftp]://path/to/jar/foo.jar
注意 1, 2, 和 3 支持通配符。例如:
file://path/to/jar/*,file://path2/to/jar/*/*.jar
hdfs://nameservice/path/to/jar/*,hdfs://nameservice2/path/to/jar/*/*.jar
3.1.0
spark.sql.hive.metastore.sharedPrefixescom.mysql.jdbc,
org.postgresql,
com.microsoft.sqlserver,
oracle.jdbc
应以在 Spark SQL 和特定版本 Hive 之间共享的类加载器加载的类前缀的逗号分隔列表。应共享的类的示例是与元存储通信所需的 JDBC 驱动程序。需要共享的其他类是与已共享的类交互的类。例如,log4j 使用的自定义追加器。1.4.0
spark.sql.hive.metastore.barrierPrefixes(empty)类前缀的逗号分隔列表,对于 Spark SQL 正在与之通信的每个 Hive 版本,应显式重新加载这些前缀。例如,在通常共享的前缀(即 org.apache.spark.*)中声明的 Hive UDF。1.4.0

JDBC 连接其他数据库

Spark SQL 还包含一个可以使用 JDBC 从其他数据库读取数据的数据源。此功能应优先于使用 JdbcRDD。这是因为结果以 DataFrame 形式返回,并且可以轻松地在 Spark SQL 中处理或与其他数据源连接。JDBC 数据源从 Java 或 Python 使用起来也更简单,因为它不需要用户提供 ClassTag。(请注意,这与 Spark SQL JDBC 服务器不同,后者允许其他应用程序使用 Spark SQL 运行查询)。

要开始使用,您需要将特定数据库的 JDBC 驱动程序包含在 Spark 的类路径中。例如,要从 Spark Shell 连接到 postgres,您可以运行以下命令:

./bin/spark-shell --driver-class-path postgresql-9.4.1207.jar --jars postgresql-9.4.1207.jar
数据源选项

Spark 为 JDBC 支持以下不区分大小写的选项。JDBC 的数据源选项可以通过以下方式设置:

  • DataFrameReader / DataFrameWriter.option/.options 方法
  • CREATE TABLE USING DATA_SOURCEOPTIONS 子句

对于连接属性,用户可以在数据源选项中指定 JDBC 连接属性。userpassword 通常作为登录数据源的连接属性提供。

属性名称默认值含义适用范围
url(none)要连接的 JDBC URL,格式为 jdbc:subprotocol:subname。特定于源的连接属性可以在 URL 中指定。例如:jdbc:postgresql://localhost/test?user=fred&password=secret读/写
dbtable(none)应从中读取或写入的 JDBC 表。请注意,在读取路径中使用时,SQL 查询的 FROM 子句中有效的任何内容都可以使用。例如,除了完整的表,您也可以使用括号内的子查询。不允许同时指定 dbtablequery 选项。读/写
query(none)用于将数据读入 Spark 的查询。指定的查询将被加上括号并用作 FROM 子句中的子查询。Spark 还会为子查询子句分配一个别名。例如,Spark 将向 JDBC 源发出以下形式的查询:
SELECT <columns> FROM (<user_specified_query>) spark_gen_alias
使用此选项时有几个限制:
• 不允许同时指定 dbtablequery 选项。
• 不允许同时指定 querypartitionColumn 选项。当需要指定 partitionColumn 选项时,可以使用 dbtable 选项指定子查询,并使用作为 dbtable 一部分提供的子查询别名来限定分区列。
示例:
spark.read.format("jdbc")
.option("url", jdbcUrl)
.option("query", "select c1, c2 from t1")
.load()
读/写
prepareQuery(none)将与 query 一起构成最终查询的前缀。由于指定的查询将作为 FROM 子句中的子查询加上括号,并且某些数据库不支持子查询中的所有子句,prepareQuery 属性提供了一种运行此类复杂查询的方法。例如,Spark 将向 JDBC 源发出以下形式的查询:
<prepareQuery> SELECT <columns> FROM (<user_specified_query>) spark_gen_alias
以下是几个示例:
• MSSQL Server 不接受子查询中的 WITH 子句,但可以将此类查询拆分为 prepareQueryquery
spark.read.format("jdbc")
.option("url", jdbcUrl)
.option("prepareQuery", "WITH t AS (SELECT x, y FROM tbl)")
.option("query", "SELECT * FROM t WHERE x > 10")
.load()
• MSSQL Server 不接受子查询中的临时表子句,但可以将此类查询拆分为 prepareQueryquery
spark.read.format("jdbc")
.option("url", jdbcUrl)
.option("prepareQuery", "(SELECT * INTO #TempTable FROM (SELECT * FROM tbl) t)")
.option("query", "SELECT * FROM #TempTable")
.load()
读/写
driver(none)用于连接到此 URL 的 JDBC 驱动程序的类名。读/写
partitionColumn, lowerBound, upperBound(none)如果指定了其中任何一个,则必须全部指定这些选项。此外,必须指定 numPartitions。它们描述了在从多个工作节点并行读取时如何对表进行分区。partitionColumn 必须是相关表中的数字、日期或时间戳列。请注意,lowerBoundupperBound 仅用于确定分区步长,而不用于过滤表中的行。因此,表中的所有行都将被分区并返回。此选项仅适用于读取。
示例:
spark.read.format("jdbc")
.option("url", jdbcUrl)
.option("dbtable", "(select c1, c2 from t1) as subq")
.option("partitionColumn", "c1")
.option("lowerBound", "1")
.option("upperBound", "100")
.option("numPartitions", "3")
.load()
读取
numPartitions(none)可用于表读取和写入并行性的最大分区数。这也决定了最大并发 JDBC 连接数。如果要写入的分区数超过此限制,我们会在写入前通过调用 coalesce(numPartitions) 将其减少到此限制。读/写
queryTimeout0驱动程序等待 Statement 对象执行到给定秒数的秒数。零表示没有限制。在写入路径中,此选项取决于 JDBC 驱动程序如何实现 API setQueryTimeout,例如,h2 JDBC 驱动程序检查每个查询的超时而不是整个 JDBC 批处理。读/写
fetchsize0JDBC 获取大小,它决定每次往返获取多少行。这对于默认获取大小较小的 JDBC 驱动程序(例如 Oracle 默认为 10 行)有助于提高性能。读取
batchsize1000JDBC 批处理大小,它决定每次往返插入多少行。这有助于提高 JDBC 驱动程序的性能。此选项仅适用于写入。写入
isolationLevelREAD_UNCOMMITTED事务隔离级别,适用于当前连接。它可以是 NONEREAD_COMMITTEDREAD_UNCOMMITTEDREPEATABLE_READSERIALIZABLE 之一,对应于 JDBC 的 Connection 对象定义的标准事务隔离级别,默认为 READ_UNCOMMITTED。请参阅 java.sql.Connection 中的文档。写入
sessionInitStatement(none)在向远程数据库打开每个数据库会话之后、开始读取数据之前,此选项执行自定义 SQL 语句(或 PL/SQL 块)。使用它来实现会话初始化代码。示例:option("sessionInitStatement", """BEGIN execute immediate 'alter session set "_serial_direct_read"=true'; END;""")读取
truncatefalse这是一个与 JDBC 写入器相关的选项。当启用 SaveMode.Overwrite 时,此选项使 Spark 截断现有表而不是删除并重新创建它。这可能更高效,并防止表元数据(例如索引)被删除。但是,在某些情况下它不起作用,例如当新数据具有不同模式时。如果发生故障,用户应关闭 truncate 选项以再次使用 DROP TABLE。此外,由于不同 DBMS 中 TRUNCATE TABLE 的行为不同,使用此选项并不总是安全的。MySQLDialectDB2DialectMsSqlServerDialectDerbyDialectOracleDialect 支持此选项,而 PostgresDialect 和默认的 JDBCDirect 不支持。对于未知和不支持的 JDBCDirect,用户选项 truncate 被忽略。写入
cascadeTruncate相关 JDBC 数据库的默认级联截断行为,在每个 JDBCDialect 的 isCascadeTruncate 中指定这是一个与 JDBC 写入器相关的选项。如果启用并且 JDBC 数据库支持(目前为 PostgreSQL 和 Oracle),此选项允许执行 TRUNCATE TABLE t CASCADE(在 PostgreSQL 的情况下,执行 TRUNCATE TABLE ONLY t CASCADE 以防止无意中截断后代表)。这将影响其他表,因此应谨慎使用。写入
createTableOptions这是一个与 JDBC 写入器相关的选项。如果指定,此选项允许在创建表时设置数据库特定的表和分区选项(例如,CREATE TABLE t (name string) ENGINE=InnoDB.)。写入
createTableColumnTypes(none)创建表时使用的数据库列数据类型,而不是默认值。数据类型信息应以与 CREATE TABLE 列语法相同的格式指定(例如:"name CHAR(64), comments VARCHAR(1024)")。指定的类型应是有效的 Spark SQL 数据类型。写入
customSchema(none)用于从 JDBC 连接器读取数据的自定义模式。例如,"id DECIMAL(38, 0), name STRING"。您也可以指定部分字段,其余字段使用默认类型映射。例如,"id DECIMAL(38, 0)"。列名应与 JDBC 表的相应列名相同。用户可以指定 Spark SQL 的相应数据类型,而不是使用默认值。读取
pushDownPredicatetrue启用或禁用谓词下推到 JDBC 数据源的选项。默认值为 true,在这种情况下,Spark 会尽可能多地将过滤器下推到 JDBC 数据源。否则,如果设置为 false,则没有过滤器会被下推到 JDBC 数据源,因此所有过滤器都将由 Spark 处理。当 Spark 执行谓词过滤比 JDBC 数据源更快时,通常关闭谓词下推。读取
pushDownAggregatetrue在 V2 JDBC 数据源中启用或禁用聚合下推的选项。默认值为 true,在这种情况下,Spark 会将聚合下推到 JDBC 数据源。否则,如果设置为 false,则聚合不会下推到 JDBC 数据源。当 Spark 执行聚合比 JDBC 数据源更快时,通常关闭聚合下推。请注意,当且仅当所有聚合函数和相关过滤器都可以下推时,聚合才能被下推。如果 numPartitions 等于 1 或 group by 键与 partitionColumn 相同,Spark 会将聚合完全下推到数据源,并且不对数据源输出应用最终聚合。否则,Spark 会对数据源输出应用最终聚合。读取
pushDownLimittrue启用或禁用 LIMIT 下推到 V2 JDBC 数据源的选项。LIMIT 下推还包括 LIMIT + SORT,即 Top N 操作符。默认值为 true,在这种情况下,Spark 将 LIMIT 或带 SORT 的 LIMIT 下推到 JDBC 数据源。否则,如果设置为 false,则 LIMIT 或带 SORT 的 LIMIT 不会下推到 JDBC 数据源。如果 numPartitions 大于 1,即使 LIMIT 或带 SORT 的 LIMIT 被下推,Spark 仍然会对数据源的结果应用 LIMIT 或带 SORT 的 LIMIT。否则,如果 LIMIT 或带 SORT 的 LIMIT 被下推且 numPartitions 等于 1,Spark 将不会对数据源的结果应用 LIMIT 或带 SORT 的 LIMIT。读取
pushDownOffsettrue启用或禁用 OFFSET 下推到 V2 JDBC 数据源的选项。默认值为 true,在这种情况下,Spark 会将 OFFSET 下推到 JDBC 数据源。否则,如果设置为 false,Spark 将不会尝试将 OFFSET 下推到 JDBC 数据源。如果 pushDownOffsettruenumPartitions 等于 1,OFFSET 将被下推到 JDBC 数据源。否则,OFFSET 不会被下推,Spark 仍然会对数据源的结果应用 OFFSET。读取
pushDownTableSampletrue启用或禁用 TABLESAMPLE 下推到 V2 JDBC 数据源的选项。默认值为 true,在这种情况下,Spark 会将 TABLESAMPLE 下推到 JDBC 数据源。否则,如果值设置为 false,则 TABLESAMPLE 不会下推到 JDBC 数据源。读取
keytab(none)JDBC 客户端的 kerberos keytab 文件的位置(必须通过 spark-submit 的 --files 选项或手动预上传到所有节点)。当找到路径信息时,Spark 认为 keytab 是手动分发的,否则假定为 --files。如果同时定义了 keytabprincipal,则 Spark 尝试进行 kerberos 身份验证。读/写
principal(none)指定 JDBC 客户端的 kerberos 主体名称。如果同时定义了 keytabprincipal,则 Spark 尝试进行 kerberos 身份验证。读/写
refreshKrb5Configfalse此选项控制在建立新连接之前是否要为 JDBC 客户端刷新 kerberos 配置。如果要刷新配置,请设置为 true,否则设置为 false。默认值为 false。请注意,如果将此选项设置为 true 并尝试建立多个连接,则可能发生竞争条件。一种可能的情况如下:
1. 使用安全上下文 1 设置 refreshKrb5Config 标志
2. 为相应的 DBMS 使用 JDBC 连接提供程序
3. krb5.conf 被修改但 JVM 尚未意识到必须重新加载
4. Spark 使用安全上下文 1 成功进行身份验证
5. JVM 从修改后的 krb5.conf 加载安全上下文 2
6. Spark 恢复先前保存的安全上下文 1
7. 修改后的 krb5.conf 内容刚刚消失
读/写
connectionProvider(none)用于连接到此 URL 的 JDBC 连接提供程序的名称,例如 db2, mssql。必须是随 JDBC 数据源加载的提供程序之一。用于在多个提供程序可以处理指定的驱动程序和选项时消除歧义。所选提供程序不得被 spark.sql.sources.disabledJdbcConnProviderList 禁用。读/写
preferTimestampNTZfalse当此选项设置为 true 时,TIMESTAMP WITHOUT TIME ZONE 类型被推断为 Spark 的 TimestampNTZ 类型。否则,它被解释为 Spark 的 Timestamp 类型(相当于 TIMESTAMP WITH LOCAL TIME ZONE)。此设置仅影响 TIMESTAMP WITHOUT TIME ZONE 数据类型的推断。无论此设置如何,TIMESTAMP WITH LOCAL TIME ZONETIMESTAMP WITH TIME ZONE 数据类型都一致地被解释为 Spark 的 Timestamp 类型。读取

请注意,JDBC 驱动程序并非总是支持使用 keytab 进行 kerberos 身份验证。
在使用 keytabprincipal 配置选项之前,请确保满足以下要求:

  1. 包含的 JDBC 驱动程序版本支持使用 keytab 进行 kerberos 身份验证。
  2. 有一个内置的连接提供程序支持所使用的数据库。
  3. 以下数据库有内置的连接提供程序:
    • DB2
    • MariaDB
    • MS Sql
    • Oracle
    • PostgreSQL

如果要求不满足,请考虑使用 JdbcConnectionProvider 开发人员 API 来处理自定义身份验证。

# 注意:JDBC 加载和保存可以通过 load/save 或 jdbc 方法实现
# 从 JDBC 源加载数据
jdbcDF = spark.read \.format("jdbc") \.option("url", "jdbc:postgresql:dbserver") \.option("dbtable", "schema.tablename") \.option("user", "username") \.option("password", "password") \.load()jdbcDF2 = spark.read \.jdbc("jdbc:postgresql:dbserver", "schema.tablename",properties={"user": "username", "password": "password"})# 在读取时指定 DataFrame 列数据类型
jdbcDF3 = spark.read \.format("jdbc") \.option("url", "jdbc:postgresql:dbserver") \.option("dbtable", "schema.tablename") \.option("user", "username") \.option("password", "password") \.option("customSchema", "id DECIMAL(38, 0), name STRING") \.load()# 将数据保存到 JDBC 源
jdbcDF.write \.format("jdbc") \.option("url", "jdbc:postgresql:dbserver") \.option("dbtable", "schema.tablename") \.option("user", "username") \.option("password", "password") \.save()jdbcDF2.write \.jdbc("jdbc:postgresql:dbserver", "schema.tablename",properties={"user": "username", "password": "password"})# 在写入时指定创建表的列数据类型
jdbcDF.write \.option("createTableColumnTypes", "name CHAR(64), comments VARCHAR(1024)") \.jdbc("jdbc:postgresql:dbserver", "schema.tablename",properties={"user": "username", "password": "password"})

完整示例代码可在 Spark 代码库的 “examples/src/main/python/sql/datasource.py” 中找到。

// 注意:JDBC 加载和保存可以通过 load/save 或 jdbc 方法实现
// 从 JDBC 源加载数据
val jdbcDF = spark.read.format("jdbc").option("url", "jdbc:postgresql:dbserver").option("dbtable", "schema.tablename").option("user", "username").option("password", "password").load()val connectionProperties = new Properties()
connectionProperties.put("user", "username")
connectionProperties.put("password", "password")
val jdbcDF2 = spark.read.jdbc("jdbc:postgresql:dbserver", "schema.tablename", connectionProperties)
// 指定读取模式的自定义数据类型
connectionProperties.put("customSchema", "id DECIMAL(38, 0), name STRING")
val jdbcDF3 = spark.read.jdbc("jdbc:postgresql:dbserver", "schema.tablename", connectionProperties)// 将数据保存到 JDBC 源
jdbcDF.write.format("jdbc").option("url", "jdbc:postgresql:dbserver").option("dbtable", "schema.tablename").option("user", "username").option("password", "password").save()jdbcDF2.write.jdbc("jdbc:postgresql:dbserver", "schema.tablename", connectionProperties)// 在写入时指定创建表的列数据类型
jdbcDF.write.option("createTableColumnTypes", "name CHAR(64), comments VARCHAR(1024)").jdbc("jdbc:postgresql:dbserver", "schema.tablename", connectionProperties)

完整示例代码可在 Spark 代码库的 “examples/src/main/scala/org/apache/spark/examples/sql/SQLDataSourceExample.scala” 中找到。

// 注意:JDBC 加载和保存可以通过 load/save 或 jdbc 方法实现
// 从 JDBC 源加载数据
Dataset<Row> jdbcDF = spark.read().format("jdbc").option("url", "jdbc:postgresql:dbserver").option("dbtable", "schema.tablename").option("user", "username").option("password", "password").load();Properties connectionProperties = new Properties();
connectionProperties.put("user", "username");
connectionProperties.put("password", "password");
Dataset<Row> jdbcDF2 = spark.read().jdbc("jdbc:postgresql:dbserver", "schema.tablename", connectionProperties);// 将数据保存到 JDBC 源
jdbcDF.write().format("jdbc").option("url", "jdbc:postgresql:dbserver").option("dbtable", "schema.tablename").option("user", "username").option("password", "password").save();jdbcDF2.write().jdbc("jdbc:postgresql:dbserver", "schema.tablename", connectionProperties);// 在写入时指定创建表的列数据类型
jdbcDF.write().option("createTableColumnTypes", "name CHAR(64), comments VARCHAR(1024)").jdbc("jdbc:postgresql:dbserver", "schema.tablename", connectionProperties);

完整示例代码可在 Spark 代码库的 “examples/src/main/java/org/apache/spark/examples/sql/JavaSQLDataSourceExample.java” 中找到。

# 从 JDBC 源加载数据
df <- read.jdbc("jdbc:postgresql:dbserver", "schema.tablename", user = "username", password = "password")# 将数据保存到 JDBC 源
write.jdbc(df, "jdbc:postgresql:dbserver", "schema.tablename", user = "username", password = "password")

完整示例代码可在 Spark 代码库的 “examples/src/main/r/RSparkSQLExample.R” 中找到。

CREATE TEMPORARY VIEW jdbcTable
USING org.apache.spark.sql.jdbc
OPTIONS (url "jdbc:postgresql:dbserver",dbtable "schema.tablename",user 'username',password 'password'
)INSERT INTO TABLE jdbcTable
SELECT * FROM resultTable

Avro 文件

自 Spark 2.4 版本发布以来,Spark SQL 提供了对读写 Apache Avro 数据的内置支持。

部署

spark-avro 模块是外部模块,默认不包含在 spark-submitspark-shell 中。

与任何 Spark 应用程序一样,spark-submit 用于启动您的应用程序。可以使用 --packages 直接将 spark-avro_2.12 及其依赖项添加到 spark-submit,例如:

./bin/spark-submit --packages org.apache.spark:spark-avro_2.12:3.5.7 ...

对于在 spark-shell 上进行实验,您也可以使用 --packages 直接添加 org.apache.spark:spark-avro_2.12 及其依赖项:

./bin/spark-shell --packages org.apache.spark:spark-avro_2.12:3.5.7 ...

有关提交带有外部依赖项的应用程序的更多详细信息,请参阅应用程序提交指南

加载和保存函数

由于 spark-avro 模块是外部模块,因此在 DataFrameReaderDataFrameWriter 中没有 .avro API。

要以 Avro 格式加载/保存数据,您需要将数据源选项 format 指定为 avro(或 org.apache.spark.sql.avro)。

df = spark.read.format("avro").load("examples/src/main/resources/users.avro")
df.select("name", "favorite_color").write.format("avro").save("namesAndFavColors.avro")
val usersDF = spark.read.format("avro").load("examples/src/main/resources/users.avro")
usersDF.select("name", "favorite_color").write.format("avro").save("namesAndFavColors.avro")
Dataset<Row> usersDF = spark.read().format("avro").load("examples/src/main/resources/users.avro");
usersDF.select("name", "favorite_color").write().format("avro").save("namesAndFavColors.avro");
df <- read.df("examples/src/main/resources/users.avro", "avro")
write.df(select(df, "name", "favorite_color"), "namesAndFavColors.avro", "avro")
to_avro()from_avro()

Avro 包提供了 to_avro 函数将列编码为 Avro 格式的二进制数据,以及 from_avro() 函数将 Avro 二进制数据解码为列。这两个函数都将一个列转换为另一个列,输入/输出的 SQL 数据类型可以是复杂类型或基本类型。

当从流式源(如 Kafka)读取或写入时,使用 Avro 记录作为列非常有用。每个 Kafka 键值记录将使用一些元数据进行增强,例如摄取到 Kafka 的时间戳、Kafka 中的偏移量等。

如果包含数据的 “value” 字段是 Avro 格式,您可以使用 from_avro() 提取数据,对其进行丰富、清理,然后再次推送到下游 Kafka 或将其写出到文件。
to_avro() 可用于将结构体转换为 Avro 记录。当您希望在将数据写出到 Kafka 时将多个列重新编码为单个列时,此方法特别有用。

from pyspark.sql.avro.functions import from_avro, to_avro# `from_avro` 需要 JSON 字符串格式的 Avro 模式。
jsonFormatSchema = open("examples/src/main/resources/user.avsc", "r").read()df = spark\.readStream\.format("kafka")\.option("kafka.bootstrap.servers", "host1:port1,host2:port2")\.option("subscribe", "topic1")\.load()# 1. 将 Avro 数据解码为结构体;
# 2. 按列 `favorite_color` 过滤;
# 3. 将列 `name` 编码为 Avro 格式。
output = df\.select(from_avro("value", jsonFormatSchema).alias("user"))\.where('user.favorite_color == "red"')\.select(to_avro("user.name").alias("value"))query = output\.writeStream\.format("kafka")\.option("kafka.bootstrap.servers", "host1:port1,host2:port2")\.option("topic", "topic2")\.start()
import org.apache.spark.sql.avro.functions._// `from_avro` 需要 JSON 字符串格式的 Avro 模式。
val jsonFormatSchema = new String(Files.readAllBytes(Paths.get("./examples/src/main/resources/user.avsc")))val df = spark.readStream.format("kafka").option("kafka.bootstrap.servers", "host1:port1,host2:port2").option("subscribe", "topic1").load()// 1. 将 Avro 数据解码为结构体;
// 2. 按列 `favorite_color` 过滤;
// 3. 将列 `name` 编码为 Avro 格式。
val output = df.select(from_avro($"value", jsonFormatSchema) as $"user").where("user.favorite_color == \"red\"").select(to_avro($"user.name") as $"value")val query = output.writeStream.format("kafka").option("kafka.bootstrap.servers", "host1:port1,host2:port2").option("topic", "topic2").start()
import static org.apache.spark.sql.functions.col;
import static org.apache.spark.sql.avro.functions.*;// `from_avro` 需要 JSON 字符串格式的 Avro 模式。
String jsonFormatSchema = new String(Files.readAllBytes(Paths.get("./examples/src/main/resources/user.avsc")));Dataset<Row> df = spark.readStream().format("kafka").option("kafka.bootstrap.servers", "host1:port1,host2:port2").option("subscribe", "topic1").load();// 1. 将 Avro 数据解码为结构体;
// 2. 按列 `favorite_color` 过滤;
// 3. 将列 `name` 编码为 Avro 格式。
Dataset<Row> output = df.select(from_avro(col("value"), jsonFormatSchema).as("user")).where("user.favorite_color == \"red\"").select(to_avro(col("user.name")).as("value"));StreamingQuery query = output.writeStream().format("kafka").option("kafka.bootstrap.servers", "host1:port1,host2:port2").option("topic", "topic2").start();
# `from_avro` 需要 JSON 字符串格式的 Avro 模式。
jsonFormatSchema <- paste0(readLines("examples/src/main/resources/user.avsc"), collapse=" ")df <- read.stream("kafka",kafka.bootstrap.servers = "host1:port1,host2:port2",subscribe = "topic1"
)# 1. 将 Avro 数据解码为结构体;
# 2. 按列 `favorite_color` 过滤;
# 3. 将列 `name` 编码为 Avro 格式。output <- select(filter(select(df, alias(from_avro("value", jsonFormatSchema), "user")),column("user.favorite_color") == "red"),alias(to_avro("user.name"), "value")
)write.stream(output,"kafka",kafka.bootstrap.servers = "host1:port1,host2:port2",topic = "topic2"
)
数据源选项

Avro 的数据源选项可以通过以下方式设置:

  • DataFrameReaderDataFrameWriter 上的 .option 方法。
  • from_avro 函数中的 options 参数。
属性名称默认值含义适用范围引入版本
avroSchemaNone用户以 JSON 格式提供的可选模式。
• 在读取 Avro 文件或调用 from_avro 函数时,此选项可以设置为一个演进模式,该模式与实际 Avro 模式兼容但不同。反序列化模式将与演进模式保持一致。例如,如果我们设置一个包含一个带有默认值的附加列的演进模式,Spark 中的读取结果也将包含新列。请注意,当将此选项与 from_avro 一起使用时,您仍然需要将实际的 Avro 模式作为参数传递给函数。
• 在写入 Avro 时,如果预期的输出 Avro 模式与 Spark 转换的模式不匹配,可以设置此选项。例如,某个列的预期模式是 “enum” 类型,而不是默认转换模式中的 “string” 类型。
读取、写入和函数 from_avro2.4.0
recordNametopLevelRecord写入结果中的顶级记录名称,这是 Avro 规范所要求的。写入2.4.0
recordNamespace""写入结果中的记录命名空间。写入2.4.0
ignoreExtensiontrue此选项控制在读取时是否忽略没有 .avro 扩展名的文件。
如果启用此选项,则加载所有文件(有和没有 .avro 扩展名的)。
此选项已被弃用,并将在未来版本中移除。请使用通用数据源选项 pathGlobFilter 来过滤文件名。
读取2.4.0
compressionsnappycompression 选项允许指定写入时使用的压缩编解码器。
当前支持的编解码器有:uncompressed, snappy, deflate, bzip2, xzzstandard
如果未设置此选项,则考虑配置 spark.sql.avro.compression.codec
写入2.4.0
modeFAILFASTmode 选项允许为 from_avro 函数指定解析模式。
当前支持的模式有:
FAILFAST:在处理损坏记录时抛出异常。
PERMISSIVE:损坏记录作为 null 结果处理。因此,数据模式被强制为完全可空,这可能与用户提供的模式不同。
函数 from_avro2.4.0
datetimeRebaseModespark.sql.avro.datetimeRebaseModeInRead 配置的值datetimeRebaseMode 选项允许为从儒略历到公历的日期、timestamp-micros、timestamp-millis 逻辑类型的值指定重定基准模式。
当前支持的模式有:
EXCEPTION:在读取两个日历之间不明确的古老日期/时间戳时失败。
CORRECTED:加载日期/时间戳而不进行重定基准。
LEGACY:将古老日期/时间戳从儒略历重定基准到公历。
读取和函数 from_avro3.2.0
positionalFieldMatchingfalse此选项可与 avroSchema 选项结合使用,以调整将提供的 Avro 模式中的字段与 SQL 模式中的字段进行匹配的行为。默认情况下,匹配将使用字段名称执行,忽略它们的位置。如果此选项设置为 “true”,则匹配将基于字段的位置。读取和写入3.2.0
enableStableIdentifiersForUnionTypefalse如果设置为 true,Avro 模式将被反序列化为 Spark SQL 模式,并且 Avro Union 类型将转换为一个结构体,其中字段名称与其各自类型保持一致。生成的字段名称将转换为小写,例如 member_intmember_string。如果两个用户定义的类型名称或一个用户定义的类型名称与一个内置类型名称相同(不区分大小写),将引发异常。然而,在其他情况下,字段名称可以唯一标识。读取3.5.0
配置

可以使用 SparkSession 上的 setConf 方法或通过使用 SQL 运行 SET key=value 命令来完成 Avro 的配置。

属性名称默认值含义引入版本
spark.sql.legacy.replaceDatabricksSparkAvro.enabledtrue如果设置为 true,为了向后兼容,数据源提供程序 com.databricks.spark.avro 将映射到内置但外部的 Avro 数据源模块。
注意:此 SQL 配置在 Spark 3.2 中已被弃用,并可能在未来移除。
2.4.0
spark.sql.avro.compression.codecsnappy写入 AVRO 文件时使用的压缩编解码器。支持的编解码器:uncompressed, deflate, snappy, bzip2, xzzstandard。默认编解码器是 snappy2.4.0
spark.sql.avro.deflate.level-1写入 AVRO 文件时使用的 deflate 编解码器的压缩级别。有效值必须在 1 到 9(包含)的范围内或为 -1。默认值为 -1,在当前实现中对应于级别 6。2.4.0
spark.sql.avro.datetimeRebaseModeInReadEXCEPTION从儒略历到公历的日期、timestamp-micros、timestamp-millis 逻辑类型的值的重定基准模式:
EXCEPTION:如果 Spark 遇到两个日历之间不明确的古老日期/时间戳,将使读取失败。
CORRECTED:Spark 不会进行重定基准,并按原样读取日期/时间戳。
LEGACY:在读取 Avro 文件时,Spark 将日期/时间戳从传统的混合(儒略历+公历)日历重定基准到公历。
仅当 Avro 文件的写入者信息(如 Spark、Hive)未知时,此配置才有效。
3.0.0
spark.sql.avro.datetimeRebaseModeInWriteEXCEPTION从公历到儒略历的日期、timestamp-micros、timestamp-millis 逻辑类型的值的重定基准模式:
EXCEPTION:如果 Spark 遇到两个日历之间不明确的古老日期/时间戳,将使写入失败。
CORRECTED:Spark 不会进行重定基准,并按原样写入日期/时间戳。
LEGACY:在写入 Avro 文件时,Spark 将日期/时间戳从公历重定基准到传统的混合(儒略历+公历)日历。
3.0.0
spark.sql.avro.filterPushdown.enabledtrue当为 true 时,启用对 Avro 数据源的过滤器下推。3.1.0
与 Databricks spark-avro 的兼容性

此 Avro 数据源模块最初来自 Databricks 的开源仓库 spark-avro 并与之兼容。

默认情况下,启用 SQL 配置 spark.sql.legacy.replaceDatabricksSparkAvro.enabled 时,数据源提供程序 com.databricks.spark.avro 将映射到此内置的 Avro 模块。对于在目录元存储中 Provider 属性为 com.databricks.spark.avro 的 Spark 表,如果您使用此内置 Avro 模块,则此映射对于加载这些表至关重要。

请注意,在 Databricks 的 spark-avro 中,为快捷函数 .avro() 创建了隐式类 AvroDataFrameWriterAvroDataFrameReader。在此内置但外部的模块中,这两个隐式类已被移除。请改用 DataFrameWriterDataFrameReader 中的 .format("avro"),这应该足够清晰和好用。

如果您更喜欢使用自己构建的 spark-avro jar 文件,您可以简单地禁用配置 spark.sql.legacy.replaceDatabricksSparkAvro.enabled,并在部署应用程序时使用 --jars 选项。有关更多详细信息,请阅读应用程序提交指南中的高级依赖管理部分。

Avro -> Spark SQL 转换支持的类型

目前 Spark 支持读取 Avro 记录下的所有基本类型和复杂类型。

Avro 类型Spark SQL 类型
booleanBooleanType
intIntegerType
longLongType
floatFloatType
doubleDoubleType
stringStringType
enumStringType
fixedBinaryType
bytesBinaryType
recordStructType
arrayArrayType
mapMapType
union见下文

除了上面列出的类型,它还支持读取联合类型。以下三种类型被认为是基本联合类型:

  • union(int, long) 将映射到 LongType
  • union(float, double) 将映射到 DoubleType
  • union(something, null),其中 something 是任何支持的 Avro 类型。这将映射到与 something 相同的 Spark SQL 类型,且 nullable 设置为 true

所有其他联合类型都被认为是复杂的。它们将映射到 StructType,其中字段名称为 member0member1 等,与联合的成员一致。这与在 Avro 和 Parquet 之间转换时的行为一致。

它还支持读取以下 Avro 逻辑类型:

Avro 逻辑类型Avro 类型Spark SQL 类型
dateintDateType
timestamp-millislongTimestampType
timestamp-microslongTimestampType
decimalfixedDecimalType
decimalbytesDecimalType

目前,它会忽略 Avro 文件中存在的文档、别名和其他属性。

Spark SQL -> Avro 转换支持的类型

Spark 支持将所有 Spark SQL 类型写入 Avro。对于大多数类型,从 Spark 类型到 Avro 类型的映射是直接的(例如,IntegerType 转换为 int);但是,有几个特殊情况列在下面:

Spark SQL 类型Avro 类型Avro 逻辑类型
ByteTypeint
ShortTypeint
BinaryTypebytes
DateTypeintdate
TimestampTypelongtimestamp-micros
DecimalTypefixeddecimal

您还可以使用选项 avroSchema 指定整个输出 Avro 模式,以便 Spark SQL 类型可以转换为其他 Avro 类型。以下转换默认不应用,需要用户指定 Avro 模式:

Spark SQL 类型Avro 类型Avro 逻辑类型
BinaryTypefixed
StringTypeenum
TimestampTypelongtimestamp-millis
DecimalTypebytesdecimal

Protobuf 数据

自 Spark 3.4.0 版本发布以来,Spark SQL 提供了对读写 protobuf 数据的内置支持。

部署

spark-protobuf 模块是外部模块,默认不包含在 spark-submitspark-shell 中。

与任何 Spark 应用程序一样,spark-submit 用于启动您的应用程序。可以使用 --packages 直接将 spark-protobuf_2.12 及其依赖项添加到 spark-submit,例如:

./bin/spark-submit --packages org.apache.spark:spark-protobuf_2.12:3.5.7 ...

对于在 spark-shell 上进行实验,您也可以使用 --packages 直接添加 org.apache.spark:spark-protobuf_2.12 及其依赖项:

./bin/spark-shell --packages org.apache.spark:spark-protobuf_2.12:3.5.7 ...

有关提交带有外部依赖项的应用程序的更多详细信息,请参阅应用程序提交指南

to_protobuf()from_protobuf()

spark-protobuf 包提供了 to_protobuf 函数将列编码为 protobuf 格式的二进制数据,以及 from_protobuf() 函数将 protobuf 二进制数据解码为列。这两个函数都将一个列转换为另一个列,输入/输出的 SQL 数据类型可以是复杂类型或基本类型。

当从流式源(如 Kafka)读取或写入时,使用 protobuf 消息作为列非常有用。每个 Kafka 键值记录将使用一些元数据进行增强,例如摄取到 Kafka 的时间戳、Kafka 中的偏移量等。

如果包含数据的 “value” 字段是 protobuf 格式,您可以使用 from_protobuf() 提取数据,对其进行丰富、清理,然后再次推送到下游 Kafka 或将其写出到不同的接收器。
to_protobuf() 可用于将结构体转换为 protobuf 消息。当您希望在将数据写出到 Kafka 时将多个列重新编码为单个列时,此方法特别有用。
Spark SQL 模式是基于传递给 from_protobufto_protobuf 的 protobuf 描述符文件或 protobuf 类生成的。指定的 protobuf 类或 protobuf 描述符文件必须与数据匹配,否则行为是未定义的:它可能会失败或返回任意结果。

from pyspark.sql.protobuf.functions import from_protobuf, to_protobuf# from_protobuf 和 to_protobuf 提供两种模式选择:通过 Protobuf 描述符文件,
# 或通过 shaded Java 类。
# 给出输入的 .proto protobuf 模式
# syntax = "proto3"
# message AppEvent {
#   string name = 1;
#   int64 id = 2;
#   string context = 3;
# }
df = spark.readStream.format("kafka")\.option("kafka.bootstrap.servers", "host1:port1,host2:port2").option("subscribe", "topic1").load()# 1. 将模式为 `AppEvent` 的 Protobuf 数据解码为结构体;
# 2. 按列 `name` 过滤;
# 3. 将列 `event` 编码为 Protobuf 格式。
# 可以使用 Protobuf protoc 命令为给定的 .proto 文件生成 protobuf 描述符文件。
output = df.select(from_protobuf("value", "AppEvent", descriptorFilePath).alias("event")).where('event.name == "alice"').select(to_protobuf("event", "AppEvent", descriptorFilePath).alias("event"))# 或者,您可以使用 protobuf 类名将 SQL 列解码和编码为 protobuf 格式。
# 指定的 Protobuf 类必须与数据匹配,否则行为是未定义的:
# 它可能会失败或返回任意结果。为避免冲突,包含 'com.google.protobuf.*' 类的 jar 文件应被 shaded。
# 一个 shading 的示例可以在 https://github.com/rangadi/shaded-protobuf-classes 找到。
output = df.select(from_protobuf("value", "org.sparkproject.spark_protobuf.protobuf.AppEvent").alias("event")).where('event.name == "alice"')output.printSchema()
# root
#  |--event: struct (nullable = true)
#  |   |-- name : string (nullable = true)
#  |   |-- id: long (nullable = true)
#  |   |-- context: string (nullable = true)output = output.select(to_protobuf("event", "org.sparkproject.spark_protobuf.protobuf.AppEvent").alias("event"))query = output.writeStream.format("kafka").option("kafka.bootstrap.servers", "host1:port1,host2:port2")\.option("topic", "topic2").start()
import org.apache.spark.sql.protobuf.functions._// `from_protobuf` 和 `to_protobuf` 提供两种模式选择:通过 protobuf 描述符文件,
// 或通过 shaded Java 类。
// 给出输入的 .proto protobuf 模式
// syntax = "proto3"
// message AppEvent {
//   string name = 1;
//   int64 id = 2;
//   string context = 3;
// }val df = spark.readStream.format("kafka").option("kafka.bootstrap.servers", "host1:port1,host2:port2").option("subscribe", "topic1").load()// 1. 将模式为 `AppEvent` 的 Protobuf 数据解码为结构体;
// 2. 按列 `name` 过滤;
// 3. 将列 `event` 编码为 Protobuf 格式。
// 可以使用 Protobuf protoc 命令为给定的 .proto 文件生成 protobuf 描述符文件。
val output = df.select(from_protobuf($"value", "AppEvent", descriptorFilePath) as $"event").where("event.name == \"alice\"").select(to_protobuf($"user", "AppEvent", descriptorFilePath) as $"event")val query = output.writeStream.format("kafka").option("kafka.bootstrap.servers", "host1:port1,host2:port2").option("topic", "topic2").start()// 或者,您可以使用 protobuf 类名将 SQL 列解码和编码为 protobuf 格式。
// 指定的 Protobuf 类必须与数据匹配,否则行为是未定义的:
// 它可能会失败或返回任意结果。为避免冲突,包含 'com.google.protobuf.*' 类的 jar 文件应被 shaded。
// 一个 shading 的示例可以在 https://github.com/rangadi/shaded-protobuf-classes 找到。
var output = df.select(from_protobuf($"value", "org.example.protos..AppEvent") as $"event").where("event.name == \"alice\"")output.printSchema()
// root
//  |--event: struct (nullable = true)
//  |    |-- name : string (nullable = true)
//  |    |-- id: long (nullable = true)
//  |    |-- context: string (nullable = true)output = output.select(to_protobuf($"event", "org.sparkproject.spark_protobuf.protobuf.AppEvent") as $"event")val query = output.writeStream.format("kafka").option("kafka.bootstrap.servers", "host1:port1,host2:port2").option("topic", "topic2").start()
import static org.apache.spark.sql.functions.col;
import static org.apache.spark.sql.protobuf.functions.*;// `from_protobuf` 和 `to_protobuf` 提供两种模式选择:通过 protobuf 描述符文件,
// 或通过 shaded Java 类。
// 给出输入的 .proto protobuf 模式
// syntax = "proto3"
// message AppEvent {
//   string name = 1;
//   int64 id = 2;
//   string context = 3;
// }Dataset<Row> df = spark.readStream().format("kafka").option("kafka.bootstrap.servers", "host1:port1,host2:port2").option("subscribe", "topic1").load();// 1. 将模式为 `AppEvent` 的 Protobuf 数据解码为结构体;
// 2. 按列 `name` 过滤;
// 3. 将列 `event` 编码为 Protobuf 格式。
// 可以使用 Protobuf protoc 命令为给定的 .proto 文件生成 protobuf 描述符文件。
Dataset<Row> output = df.select(from_protobuf(col("value"), "AppEvent", descriptorFilePath).as("event")).where("event.name == \"alice\"").select(to_protobuf(col("event"), "AppEvent", descriptorFilePath).as("event"));// 或者,您可以使用 protobuf 类名将 SQL 列解码和编码为 protobuf 格式。
// 指定的 Protobuf 类必须与数据匹配,否则行为是未定义的:
// 它可能会失败或返回任意结果。为避免冲突,包含 'com.google.protobuf.*' 类的 jar 文件应被 shaded。
// 一个 shading 的示例可以在 https://github.com/rangadi/shaded-protobuf-classes 找到。
Dataset<Row> output = df.select(from_protobuf(col("value"),"org.sparkproject.spark_protobuf.protobuf.AppEvent").as("event")).where("event.name == \"alice\"")output.printSchema()
// root
//  |--event: struct (nullable = true)
//  |    |-- name : string (nullable = true)
//  |    |-- id: long (nullable = true)
//  |    |-- context: string (nullable = true)output = output.select(to_protobuf(col("event"),"org.sparkproject.spark_protobuf.protobuf.AppEvent").as("event"));StreamingQuery query = output.writeStream().format("kafka").option("kafka.bootstrap.servers", "host1:port1,host2:port2").option("topic", "topic2").start();
Protobuf -> Spark SQL 转换支持的类型

目前 Spark 支持读取 Protobuf 消息下的 protobuf 标量类型、枚举类型、嵌套类型和映射类型。除了这些类型,spark-protobuf 还引入了对 Protobuf OneOf 字段的支持,这允许您处理可以具有多组可能字段但一次只能出现一组字段的消息。这对于您处理的数据并不总是具有相同格式,并且需要能够处理具有不同字段集的消息而不会遇到错误的情况非常有用。

Protobuf 类型Spark SQL 类型
booleanBooleanType
intIntegerType
longLongType
floatFloatType
doubleDoubleType
stringStringType
enumStringType
bytesBinaryType
MessageStructType
repeatedArrayType
mapMapType
OneOfStruct

它还支持读取以下 Protobuf 类型 Timestamp 和 Duration:

Protobuf 逻辑类型Protobuf 模式Spark SQL 类型
durationMessageType{seconds: Long, nanos: Int}DayTimeIntervalType
timestampMessageType{seconds: Long, nanos: Int}TimestampType
Spark SQL -> Protobuf 转换支持的类型

Spark 支持将所有 Spark SQL 类型写入 Protobuf。对于大多数类型,从 Spark 类型到 Protobuf 类型的映射是直接的(例如,IntegerType 转换为 int):

Spark SQL 类型Protobuf 类型
BooleanTypeboolean
IntegerTypeint
LongTypelong
FloatTypefloat
DoubleTypedouble
StringTypestring
StringTypeenum
BinaryTypebytes
StructTypemessage
ArrayTyperepeated
MapTypemap
处理 protobuf 字段的循环引用

处理 Protobuf 数据时可能出现的一个常见问题是存在循环引用。在 Protobuf 中,当字段引用回自身或引用回原始字段的另一个字段时,就会发生循环引用。这可能在解析数据时导致问题,因为它可能导致无限循环或其他意外行为。为了解决这个问题,最新版本的 spark-protobuf 引入了一个新特性:能够通过字段类型检查循环引用。这允许用户使用 recursive.fields.max.depth 选项来指定解析模式时允许的最大递归级别数。默认情况下,spark-protobuf 通过将 recursive.fields.max.depth 设置为 -1 来不允许递归字段。但是,如果需要,您可以将其设置为 0 到 10。

recursive.fields.max.depth 设置为 0 会丢弃所有递归字段,设置为 1 允许递归一次,设置为 2 允许递归两次。不允许 recursive.fields.max.depth 值大于 10,因为它可能导致性能问题甚至堆栈溢出。

以下 protobuf 消息的 SQL 模式将根据 recursive.fields.max.depth 的值而变化。

syntax = "proto3"
message Person {string name = 1;Person bff = 2
}// 上面定义的 protobuf 模式将根据 `recursive.fields.max.depth` 值转换为具有以下结构的 Spark SQL 列。0: struct<name: string, bff: null>
1: struct<name string, bff: <name: string, bff: null>>
2: struct<name string, bff: <name: string, bff: struct<name: string, bff: null>>> ...

二进制文件

自 Spark 3.0 起,Spark 支持二进制文件数据源,该数据源读取二进制文件并将每个文件转换为包含文件原始内容和元数据的单个记录。它生成一个包含以下列以及可能的分区列的 DataFrame:

  • path: StringType
  • modificationTime: TimestampType
  • length: LongType
  • content: BinaryType

要读取整个二进制文件,您需要将数据源格式指定为 binaryFile。为了加载路径匹配给定通配符模式的文件,同时保持分区发现的行为,您可以使用通用数据源选项 pathGlobFilter。例如,以下代码从输入目录读取所有 PNG 文件:

spark.read.format("binaryFile").option("pathGlobFilter", "*.png").load("/path/to/data")
spark.read.format("binaryFile").option("pathGlobFilter", "*.png").load("/path/to/data")
spark.read().format("binaryFile").option("pathGlobFilter", "*.png").load("/path/to/data");
read.df("/path/to/data", source = "binaryFile", pathGlobFilter = "*.png")

二进制文件数据源不支持将 DataFrame 写回原始文件。

故障排除

  • JDBC 驱动类必须对客户端会话和所有执行器上的原始类加载器可见。这是因为 Java 的 DriverManager 类会执行安全检查,导致在尝试打开连接时忽略所有对原始类加载器不可见的驱动程序。一种便捷的方法是在所有工作节点上修改 compute_classpath.sh 脚本,以包含您的驱动程序 JAR 文件。

  • 某些数据库(例如 H2)会将所有名称转换为大写。您需要在 Spark SQL 中使用大写来引用这些名称。

  • 用户可以在数据源选项中指定特定于数据库供应商的 JDBC 连接属性,以进行特殊处理。例如:

    spark.read.format("jdbc").option("url", oracleJdbcUrl).option("oracle.jdbc.mapDateToTimestamp", "false")
    

    oracle.jdbc.mapDateToTimestamp 默认为 true,用户通常需要禁用此标志,以避免 Oracle 日期被解析为时间戳类型。

性能调优

对于某些工作负载,可以通过在内存中缓存数据或开启一些实验性选项来提高性能。

内存中缓存数据

Spark SQL 可以通过调用 spark.catalog.cacheTable("tableName")dataFrame.cache() 来使用内存列式格式缓存表。然后 Spark SQL 将仅扫描所需的列,并自动调整压缩以最小化内存使用和 GC 压力。您可以调用 spark.catalog.uncacheTable("tableName")dataFrame.unpersist() 来从内存中移除表。

可以使用 SparkSession 上的 setConf 方法或通过使用 SQL 运行 SET key=value 命令来配置内存中缓存。

属性名称默认值含义引入版本
spark.sql.inMemoryColumnarStorage.compressedtrue当设置为 true 时,Spark SQL 会根据数据的统计信息自动为每列选择压缩编解码器。1.0.1
spark.sql.inMemoryColumnarStorage.batchSize10000控制列式缓存的批处理大小。较大的批处理大小可以提高内存利用率和压缩率,但在缓存数据时有 OOM 风险。1.1.1

其他配置选项

以下选项也可用于调整查询执行的性能。随着更多优化自动执行,这些选项可能会在未来的版本中被弃用。

属性名称默认值含义引入版本
spark.sql.files.maxPartitionBytes134217728 (128 MB)读取文件时打包到单个分区中的最大字节数。此配置仅在使用基于文件的数据源(如 Parquet、JSON 和 ORC)时有效。2.0.0
spark.sql.files.openCostInBytes4194304 (4 MB)打开文件的估算成本,以在相同时间内可以扫描的字节数衡量。这在将多个文件放入一个分区时使用。最好高估此值,这样包含小文件的分区将比包含大文件的分区(先调度)更快。此配置仅在使用基于文件的数据源(如 Parquet、JSON 和 ORC)时有效。2.0.0
spark.sql.files.minPartitionNum默认并行度建议(非保证)的最小拆分文件分区数。如果未设置,默认值为 spark.sql.leafNodeDefaultParallelism。此配置仅在使用基于文件的数据源(如 Parquet、JSON 和 ORC)时有效。3.1.0
spark.sql.files.maxPartitionNumNone建议(非保证)的最大拆分文件分区数。如果设置了此值,并且初始分区数超过此值,Spark 将重新缩放每个分区,使分区数接近此值。此配置仅在使用基于文件的数据源(如 Parquet、JSON 和 ORC)时有效。3.5.0
spark.sql.broadcastTimeout300广播连接中广播等待的超时时间(秒)。1.3.0
spark.sql.autoBroadcastJoinThreshold10485760 (10 MB)配置在执行连接时将广播到所有工作节点的表的最大大小(字节)。通过将此值设置为 -1,可以禁用广播。请注意,目前仅支持对已运行 ANALYZE TABLE <tableName> COMPUTE STATISTICS noscan 命令的 Hive Metastore 表进行统计。1.1.0
spark.sql.shuffle.partitions200配置在为连接或聚合进行数据洗牌时使用的分区数。1.1.0
spark.sql.sources.parallelPartitionDiscovery.threshold32配置启用作业输入路径并行列表的阈值。如果输入路径的数量大于此阈值,Spark 将使用 Spark 分布式作业列出文件。否则,它将回退到顺序列表。此配置仅在使用基于文件的数据源(如 Parquet、ORC 和 JSON)时有效。1.5.0
spark.sql.sources.parallelPartitionDiscovery.parallelism10000配置作业输入路径的最大列表并行度。如果输入路径的数量大于此值,它将受到限制以使用此值。此配置仅在使用基于文件的数据源(如 Parquet、ORC 和 JSON)时有效。2.1.1

SQL 查询的连接策略提示

连接策略提示,即 BROADCASTMERGESHUFFLE_HASHSHUFFLE_REPLICATE_NL,指示 Spark 在将每个指定的关系与另一个关系连接时使用提示的策略。例如,当在表 ‘t1’ 上使用 BROADCAST 提示时,即使统计信息建议的表 ‘t1’ 的大小超过配置 spark.sql.autoBroadcastJoinThreshold,Spark 也会优先考虑使用 ‘t1’ 作为构建端的广播连接(广播哈希连接或广播嵌套循环连接,取决于是否存在等值连接键)。

当在连接的两侧指定不同的连接策略提示时,Spark 会优先考虑 BROADCAST 提示,其次是 MERGE 提示,然后是 SHUFFLE_HASH 提示,最后是 SHUFFLE_REPLICATE_NL 提示。当两侧都指定了 BROADCAST 提示或 SHUFFLE_HASH 提示时,Spark 将根据连接类型和关系的大小来选择构建端。

请注意,不能保证 Spark 会选择提示中指定的连接策略,因为特定策略可能不支持所有连接类型。

spark.table("src").join(spark.table("records").hint("broadcast"), "key").show()

更多详细信息请参阅连接提示的文档。

SQL 查询的合并提示

合并提示允许 Spark SQL 用户控制输出文件的数量,就像 Dataset API 中的 coalescerepartitionrepartitionByRange 一样,它们可用于性能调优和减少输出文件的数量。“COALESCE” 提示只有一个分区号作为参数。“REPARTITION” 提示可以有分区号、列,或两者都有/都没有作为参数。“REPARTITION_BY_RANGE” 提示必须有列名,分区号是可选的。“REBALANCE” 提示可以有初始分区号、列,或两者都有/都没有作为参数。

SELECT /*+ COALESCE(3) */ * FROM t;
SELECT /*+ REPARTITION(3) */ * FROM t;
SELECT /*+ REPARTITION(c) */ * FROM t;
SELECT /*+ REPARTITION(3, c) */ * FROM t;
SELECT /*+ REPARTITION */ * FROM t;
SELECT /*+ REPARTITION_BY_RANGE(c) */ * FROM t;
SELECT /*+ REPARTITION_BY_RANGE(3, c) */ * FROM t;
SELECT /*+ REBALANCE */ * FROM t;
SELECT /*+ REBALANCE(3) */ * FROM t;
SELECT /*+ REBALANCE(c) */ * FROM t;
SELECT /*+ REBALANCE(3, c) */ * FROM t;

更多详细信息请参阅分区提示的文档。

自适应查询执行

自适应查询执行(AQE)是 Spark SQL 中的一种优化技术,它利用运行时统计信息来选择最有效的查询执行计划,自 Apache Spark 3.2.0 起默认启用。Spark SQL 可以通过 spark.sql.adaptive.enabled 作为总配置来开启和关闭 AQE。截至 Spark 3.0,AQE 有三个主要特性:包括合并洗牌后分区、将排序合并连接转换为广播连接,以及倾斜连接优化。

合并洗牌后分区

spark.sql.adaptive.enabledspark.sql.adaptive.coalescePartitions.enabled 配置都为 true 时,此功能会根据映射输出统计信息合并洗牌后的分区。此功能简化了运行查询时洗牌分区数的调优。您不需要设置合适的洗牌分区数来适应您的数据集。一旦您通过 spark.sql.adaptive.coalescePartitions.initialPartitionNum 配置设置了足够大的初始洗牌分区数,Spark 就可以在运行时选择合适的洗牌分区数。

属性名称默认值含义引入版本
spark.sql.adaptive.coalescePartitions.enabledtrue当为 true 且 spark.sql.adaptive.enabled 为 true 时,Spark 将根据目标大小(由 spark.sql.adaptive.advisoryPartitionSizeInBytes 指定)合并连续的洗牌分区,以避免过多的小任务。3.0.0
spark.sql.adaptive.coalescePartitions.parallelismFirsttrue当为 true 时,Spark 在合并连续的洗牌分区时忽略 spark.sql.adaptive.advisoryPartitionSizeInBytes(默认 64MB)指定的目标大小,仅遵循 spark.sql.adaptive.coalescePartitions.minPartitionSize(默认 1MB)指定的最小分区大小,以最大化并行度。这是为了避免启用自适应查询执行时的性能回归。建议将此配置设置为 false 并遵循 spark.sql.adaptive.advisoryPartitionSizeInBytes 指定的目标大小。3.2.0
spark.sql.adaptive.coalescePartitions.minPartitionSize1MB合并后洗牌分区的最小大小。当在分区合并期间忽略目标大小时(这是默认情况),这很有用。3.2.0
spark.sql.adaptive.coalescePartitions.initialPartitionNum(none)合并前的初始洗牌分区数。如果未设置,则等于 spark.sql.shuffle.partitions。此配置仅在 spark.sql.adaptive.enabledspark.sql.adaptive.coalescePartitions.enabled 都启用时有效。3.0.0
spark.sql.adaptive.advisoryPartitionSizeInBytes64 MB自适应优化期间(当 spark.sql.adaptive.enabled 为 true 时)洗牌分区的建议大小(字节)。它在 Spark 合并小洗牌分区或拆分倾斜洗牌分区时生效。3.0.0
拆分倾斜的洗牌分区
属性名称默认值含义引入版本
spark.sql.adaptive.optimizeSkewsInRebalancePartitions.enabledtrue当为 true 且 spark.sql.adaptive.enabled 为 true 时,Spark 将优化 RebalancePartitions 中的倾斜洗牌分区,并根据目标大小(由 spark.sql.adaptive.advisoryPartitionSizeInBytes 指定)将它们拆分为更小的分区,以避免数据倾斜。3.2.0
spark.sql.adaptive.rebalancePartitionsSmallPartitionFactor0.2如果一个分区的大小小于此因子乘以 spark.sql.adaptive.advisoryPartitionSizeInBytes,则在拆分期间将合并该分区。3.3.0
将排序合并连接转换为广播连接

当任何连接侧的运行时统计信息小于自适应广播哈希连接阈值时,AQE 会将排序合并连接转换为广播哈希连接。这不如一开始就计划广播哈希连接高效,但比继续执行排序合并连接要好,因为我们可以节省两个连接侧的排序,并本地读取洗牌文件以节省网络流量(如果 spark.sql.adaptive.localShuffleReader.enabled 为 true)。

属性名称默认值含义引入版本
spark.sql.adaptive.autoBroadcastJoinThreshold(none)配置在执行连接时将广播到所有工作节点的表的最大大小(字节)。通过将此值设置为 -1,可以禁用广播。默认值与 spark.sql.autoBroadcastJoinThreshold 相同。请注意,此配置仅用于自适应框架。3.2.0
spark.sql.adaptive.localShuffleReader.enabledtrue当为 true 且 spark.sql.adaptive.enabled 为 true 时,当不需要洗牌分区时(例如,在将排序合并连接转换为广播哈希连接之后),Spark 尝试使用本地洗牌读取器来读取洗牌数据。3.0.0
将排序合并连接转换为洗牌哈希连接

当所有洗牌后分区都小于一个阈值时,AQE 会将排序合并连接转换为洗牌哈希连接,最大阈值可以参考配置 spark.sql.adaptive.maxShuffledHashJoinLocalMapThreshold

属性名称默认值含义引入版本
spark.sql.adaptive.maxShuffledHashJoinLocalMapThreshold0配置允许构建本地哈希映射的每个分区的最大大小(字节)。如果此值不小于 spark.sql.adaptive.advisoryPartitionSizeInBytes 且所有分区大小都不大于此配置,则连接选择优先使用洗牌哈希连接而不是排序合并连接,而不管 spark.sql.join.preferSortMergeJoin 的值如何。3.2.0
优化倾斜连接

数据倾斜会严重降低连接查询的性能。此功能通过将倾斜任务拆分(并在需要时复制)为大致均匀大小的任务,动态处理排序合并连接中的倾斜。当 spark.sql.adaptive.enabledspark.sql.adaptive.skewJoin.enabled 配置都启用时生效。

属性名称默认值含义引入版本
spark.sql.adaptive.skewJoin.enabledtrue当为 true 且 spark.sql.adaptive.enabled 为 true 时,Spark 通过拆分(并在需要时复制)倾斜分区来动态处理排序合并连接中的倾斜。3.0.0
spark.sql.adaptive.skewJoin.skewedPartitionFactor5.0如果一个分区的大小大于此因子乘以中位数分区大小,并且也大于 spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes,则该分区被认为是倾斜的。3.0.0
spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes256MB如果一个分区的大小(字节)大于此阈值,并且也大于 spark.sql.adaptive.skewJoin.skewedPartitionFactor 乘以中位数分区大小,则该分区被认为是倾斜的。理想情况下,此配置应设置为大于 spark.sql.adaptive.advisoryPartitionSizeInBytes3.0.0
spark.sql.adaptive.forceOptimizeSkewedJoinfalse当为 true 时,强制启用 OptimizeSkewedJoin,这是一个自适应规则,用于优化倾斜连接以避免掉队任务,即使它会引入额外的洗牌。3.3.0

其他

属性名称默认值含义引入版本
spark.sql.adaptive.optimizer.excludedRules(none)配置在自适应优化器中要禁用的规则列表,其中规则由其规则名称指定并用逗号分隔。优化器将记录确实已被排除的规则。3.1.0
spark.sql.adaptive.customCostEvaluatorClass(none)用于自适应执行的自定义成本评估器类。如果未设置,Spark 将默认使用其自己的 SimpleCostEvaluator3.2.0

分布式 SQL 引擎

Spark SQL 还可以作为分布式查询引擎,使用其 JDBC/ODBC 或命令行接口。在这种模式下,最终用户或应用程序可以直接与 Spark SQL 交互来运行 SQL 查询,而无需编写任何代码。

运行 Thrift JDBC/ODBC 服务器

此处实现的 Thrift JDBC/ODBC 服务器对应于内置 Hive 中的 HiveServer2。您可以使用 Spark 自带的或兼容的 Hive 自带的 beeline 脚本来测试 JDBC 服务器。

要启动 JDBC/ODBC 服务器,请在 Spark 目录中运行以下命令:

./sbin/start-thriftserver.sh

此脚本接受所有 bin/spark-submit 命令行选项,以及一个 --hiveconf 选项来指定 Hive 属性。您可以运行 ./sbin/start-thriftserver.sh --help 来获取所有可用选项的完整列表。默认情况下,服务器监听 localhost:10000。您可以通过环境变量覆盖此行为,例如:

export HIVE_SERVER2_THRIFT_PORT=<listening-port>
export HIVE_SERVER2_THRIFT_BIND_HOST=<listening-host>
./sbin/start-thriftserver.sh \--master <master-uri> \...

或者使用系统属性:

./sbin/start-thriftserver.sh \--hiveconf hive.server2.thrift.port=<listening-port> \--hiveconf hive.server2.thrift.bind.host=<listening-host> \--master <master-uri>...

现在您可以使用 beeline 来测试 Thrift JDBC/ODBC 服务器:

./bin/beeline

在 beeline 中连接到 JDBC/ODBC 服务器:

beeline> !connect jdbc:hive2://localhost:10000

Beeline 将要求您输入用户名和密码。在非安全模式下,只需输入您机器上的用户名和一个空密码。对于安全模式,请遵循 beeline 文档中给出的说明。

Hive 的配置是通过将您的 hive-site.xmlcore-site.xmlhdfs-site.xml 文件放入 conf/ 目录来完成的。

您也可以使用 Hive 自带的 beeline 脚本。

Thrift JDBC 服务器还支持通过 HTTP 传输发送 thrift RPC 消息。使用以下设置作为系统属性或在 conf/ 目录下的 hive-site.xml 文件中启用 HTTP 模式:

  • hive.server2.transport.mode - 将此值设置为:http
  • hive.server2.thrift.http.port - 监听的 HTTP 端口号;默认是 10001
  • hive.server2.http.endpoint - HTTP 端点;默认是 cliservice

要进行测试,使用 beeline 在 http 模式下连接到 JDBC/ODBC 服务器:

beeline> !connect jdbc:hive2://<host>:<port>/<database>?hive.server2.transport.mode=http;hive.server2.thrift.http.path=<http_endpoint>

如果您关闭了一个会话并执行 CTAS,您必须在 hive-site.xml 中将 fs.%s.impl.disable.cache 设置为 true。更多细节请参阅 [SPARK-21067]。

运行 Spark SQL CLI

要从 shell 中使用 Spark SQL 命令行界面(CLI):

./bin/spark-sql

有关详细信息,请参阅 Spark SQL CLI。

迁移指南:SQL、Datasets 和 DataFrame

从 Spark SQL 3.5.3 升级到 3.5.4

自 Spark 3.5.4 起,当读取 SQL 表遇到 org.apache.hadoop.security.AccessControlExceptionorg.apache.hadoop.hdfs.BlockMissingException 时,即使 spark.sql.files.ignoreCorruptFiles 设置为 true,也会抛出异常并导致任务失败。

从 Spark SQL 3.5.1 升级到 3.5.2

自 3.5.2 起,MySQL JDBC 数据源将 TINYINT UNSIGNED 读取为 ShortType,而在 3.5.1 中,它被错误地读取为 ByteType

从 Spark SQL 3.5.0 升级到 3.5.1

自 Spark 3.5.1 起,MySQL JDBC 数据源将 TINYINT(n > 1)TINYINT UNSIGNED 读取为 ByteType,而在 Spark 3.5.0 及更早版本中,它们被读取为 IntegerType。要恢复之前的行为,您可以将列强制转换为旧类型。

从 Spark SQL 3.4 升级到 3.5

自 Spark 3.5 起,与 DS V2 下推相关的 JDBC 选项默认设置为 true。这些选项包括:pushDownAggregatepushDownLimitpushDownOffsetpushDownTableSample。要恢复旧行为,请将它们设置为 false。例如,将 spark.sql.catalog.your_catalog_name.pushDownAggregate 设置为 false。

自 Spark 3.5 起,Spark Thrift 服务器在取消正在运行的语句时会中断任务。要恢复之前的行为,将 spark.sql.thriftServer.interruptOnCancel 设置为 false。

自 Spark 3.5 起,Row 的 jsonprettyJson 方法已移至 ToJsonUtil

自 Spark 3.5 起,plan 字段已从 AnalysisException 移至 EnhancedAnalysisException

自 Spark 3.5 起,spark.sql.optimizer.canChangeCachedPlanOutputPartitioning 默认启用。要恢复之前的行为,将 spark.sql.optimizer.canChangeCachedPlanOutputPartitioning 设置为 false。

自 Spark 3.5 起,array_insert 函数对于负索引使用基于 1 的索引。对于索引 -1,它在输入数组的末尾插入新元素。要恢复之前的行为,将 spark.sql.legacy.negativeIndexInArrayInsert 设置为 true。

自 Spark 3.5 起,当将 Interval 类型读取为 Date 或 Timestamp 类型,或读取精度较低的 Decimal 类型时,Avro 将抛出 AnalysisException。要恢复旧行为,将 spark.sql.legacy.avro.allowIncompatibleSchema 设置为 true。

从 Spark SQL 3.3 升级到 3.4

自 Spark 3.4 起,包含的列数少于目标表的显式列列表的 INSERT INTO 命令将自动为剩余列添加相应的默认值(或为任何缺少显式分配默认值的列添加 NULL)。在 Spark 3.3 或更早版本中,这些命令会失败并返回错误,报告提供的列数与目标表中的列数不匹配。请注意,禁用 spark.sql.defaultColumn.useNullsForMissingDefaultValues 将恢复之前的行为。

自 Spark 3.4 起,来自 Teradata 的 NumberNumber(*) 将被视为 Decimal(38,18)。在 Spark 3.3 或更早版本中,来自 Teradata 的 NumberNumber(*) 被视为 Decimal(38, 0),在这种情况下,小数部分将被移除。

自 Spark 3.4 起,如果定义了数据库,v1 数据库、表、永久视图和函数标识符将包含 ‘spark_catalog’ 作为目录名称,例如,表标识符将为:spark_catalog.default.t。要恢复旧行为,将 spark.sql.legacy.v1IdentifierNoCatalog 设置为 true。

自 Spark 3.4 起,当 ANSI SQL 模式(配置 spark.sql.ansi.enabled)开启时,Spark SQL 在获取不存在的键的映射值时始终返回 NULL 结果。在 Spark 3.3 或更早版本中,会抛出错误。

自 Spark 3.4 起,SQL CLI spark-sql 不会在 AnalysisException 的错误消息前打印前缀 Error in query:

自 Spark 3.4 起,当 regex 参数为空时,split 函数会忽略尾部的空字符串。

自 Spark 3.4 起,to_binary 函数对格式错误的 str 输入抛出错误。使用 try_to_binary 可以容忍格式错误的输入并返回 NULL。
有效的 Base64 字符串应包含 base64 字母表(A-Za-z0-9+/)中的符号、可选的填充(=)和可选的空白字符。在转换过程中,空白字符会被跳过,除非它们前面有填充符号。如果存在填充,它应结束字符串并遵循 RFC 4648 § 4 中描述的规则。
有效的十六进制字符串应仅包含允许的符号(0-9A-Fa-f)。
fmt 的有效值不区分大小写:hexbase64utf-8utf8

自 Spark 3.4 起,当 Spark 创建分区但其中一些分区已存在时,仅抛出 PartitionsAlreadyExistException。在 Spark 3.3 或更早版本中,Spark 可能抛出 PartitionsAlreadyExistExceptionPartitionAlreadyExistsException

自 Spark 3.4 起,Spark 会对 ALTER PARTITION 中的分区规范进行验证,以遵循 spark.sql.storeAssignmentPolicy 的行为,如果类型转换失败,可能会抛出异常,例如,如果列 p 是 int 类型,ALTER TABLE .. ADD PARTITION(p='a')。要恢复旧行为,将 spark.sql.legacy.skipTypeValidationOnAlterPartition 设置为 true。

自 Spark 3.4 起,对于嵌套数据类型(数组、映射和结构体),默认启用向量化读取器。要恢复旧行为,将 spark.sql.orc.enableNestedColumnVectorizedReaderspark.sql.parquet.enableNestedColumnVectorizedReader 设置为 false。

自 Spark 3.4 起,CSV 数据源不支持 BinaryType。在 Spark 3.3 或更早版本中,用户可以在 CSV 数据源中写入二进制列,但 CSV 文件中的输出内容是 Object.toString(),这是无意义的;同时,如果用户读取带有二进制列的 CSV 表,Spark 会抛出 Unsupported type: binary 异常。

自 Spark 3.4 起,默认启用布隆过滤器连接。要恢复旧行为,将 spark.sql.optimizer.runtime.bloomFilter.enabled 设置为 false。

自 Spark 3.4 起,在对外部 Parquet 文件进行模式推断时,带有注解 isAdjustedToUTC=false 的 INT64 时间戳将被推断为 TimestampNTZ 类型,而不是 Timestamp 类型。要恢复旧行为,将 spark.sql.parquet.inferTimestampNTZ.enabled 设置为 false。

自 Spark 3.4 起,当 spark.sql.legacy.allowNonEmptyLocationInCTAS 设置为 true 时,CREATE TABLE AS SELECT ... 的行为从 OVERWRITE 更改为 APPEND。建议用户避免在非空表位置使用 CTAS。

从 Spark SQL 3.2 升级到 3.3

自 Spark 3.3 起,Spark SQL 中的 histogram_numeric 函数返回的输出类型是结构体(x, y)的数组,其中返回值中 ‘x’ 字段的类型从聚合函数中消耗的输入值传播而来。在 Spark 3.2 或更早版本中,‘x’ 始终是 double 类型。可选地,自 Spark 3.3 起使用配置 spark.sql.legacy.histogramNumericPropagateInputType 可以恢复之前的行为。

自 Spark 3.3 起,Spark SQL 中的 DayTimeIntervalTypeArrowWriterArrowColumnVector 开发者 API 中映射到 Arrow 的 Duration 类型。之前,DayTimeIntervalType 被映射到 Arrow 的 Interval 类型,这与 Spark SQL 映射的其他语言类型不匹配。例如,在 Java 中,DayTimeIntervalType 被映射到 java.time.Duration

自 Spark 3.3 起,函数 lpadrpad 已被重载以支持字节序列。当第一个参数是字节序列时,可选的填充模式也必须是字节序列,结果是一个 BINARY 值。在这种情况下,默认的填充模式是零字节。要恢复始终返回字符串类型的旧行为,将 spark.sql.legacy.lpadRpadAlwaysReturnString 设置为 true。

自 Spark 3.3 起,当用户指定模式并且模式包含非空字段时,Spark 会将非空模式转换为可空,用于 API DataFrameReader.schema(schema: StructType).json(jsonDataset: Dataset[String])DataFrameReader.schema(schema: StructType).csv(csvDataset: Dataset[String])。要恢复尊重可空性的旧行为,将 spark.sql.legacy.respectNullabilityInTextDatasetConversion 设置为 true。

自 Spark 3.3 起,当未指定日期或时间戳模式时,Spark 使用 CAST 表达式方法将输入字符串转换为日期/时间戳。此更改影响 CSV/JSON 数据源和分区值的解析。在 Spark 3.2 或更早版本中,当未设置日期或时间戳模式时,Spark 使用默认模式:日期为 yyyy-MM-dd,时间戳为 yyyy-MM-dd HH:mm:ss。更改后,Spark 仍然识别以下模式以及:

日期模式:

[+-]yyyy*
[+-]yyyy*-[m]m
[+-]yyyy*-[m]m-[d]d
[+-]yyyy*-[m]m-[d]d
[+-]yyyy*-[m]m-[d]d *
[+-]yyyy*-[m]m-[d]dT*

时间戳模式:

[+-]yyyy*
[+-]yyyy*-[m]m
[+-]yyyy*-[m]m-[d]d
[+-]yyyy*-[m]m-[d]d
[+-]yyyy*-[m]m-[d]d [h]h:[m]m:[s]s.[ms][ms][ms][us][us][us][zone_id]
[+-]yyyy*-[m]m-[d]dT[h]h:[m]m:[s]s.[ms][ms][ms][us][us][us][zone_id]
[h]h:[m]m:[s]s.[ms][ms][ms][us][us][us][zone_id]
T[h]h:[m]m:[s]s.[ms][ms][ms][us][us][us][zone_id]

自 Spark 3.3 起,format_string(strfmt, obj, ...)printf(strfmt, obj, ...) 中的 strfmt 不再支持使用 “0"来指定第一个参数,当使用参数索引指示参数在参数列表中的位置时,第一个参数应始终通过"1" 来指定第一个参数,当使用参数索引指示参数在参数列表中的位置时,第一个参数应始终通过 "1"来指定第一个参数,当使用参数索引指示参数在参数列表中的位置时,第一个参数应始终通过"1” 引用。

自 Spark 3.3 起,在 CSV 数据源中,null 默认作为空字符串写入。在 Spark 3.2 或更早版本中,null 作为带引号的空字符串 "" 写入。要恢复之前的行为,将 nullValue 设置为 "",或将配置 spark.sql.legacy.nullValueWrittenAsQuotedEmptyStringCsv 设置为 true。

自 Spark 3.3 起,如果函数不存在,DESCRIBE FUNCTION 会失败。在 Spark 3.2 或更早版本中,DESCRIBE FUNCTION 仍然可以运行并打印 “Function: func_name not found”。

自 Spark 3.3 起,表属性 external 成为保留属性。如果您指定 external 属性,某些命令将失败,例如 CREATE TABLE ... TBLPROPERTIESALTER TABLE ... SET TBLPROPERTIES。在 Spark 3.2 及更早版本中,表属性 external 被静默忽略。您可以将 spark.sql.legacy.notReserveProperties 设置为 true 以恢复旧行为。

自 Spark 3.3 起,如果函数名称与某个内置函数名称匹配且未限定,DROP FUNCTION 会失败。在 Spark 3.2 或更早版本中,即使名称未限定且与内置函数名称相同,DROP FUNCTION 仍然可以删除持久函数。

自 Spark 3.3 起,当从定义为 FloatTypeDoubleType 的 JSON 属性读取值时,除了已经支持的 “Infinity” 和 “-Infinity” 变体外,现在还会将字符串 “+Infinity”、“+INF” 和 “-INF” 解析为适当的值。此更改是为了提高与 Jackson 解析这些值的未引用版本的一致性。此外,现在会遵循 allowNonNumericNumbers 选项,因此如果禁用此选项,这些字符串将被视为无效。

自 Spark 3.3 起,Spark 将尝试在 INSERT OVERWRITE DIRECTORY 中使用内置数据源写入器而不是 Hive serde。仅当分别为 Parquet 和 ORC 格式启用 spark.sql.hive.convertMetastoreParquetspark.sql.hive.convertMetastoreOrc 时,此行为才有效。要恢复 Spark 3.3 之前的行为,您可以将 spark.sql.hive.convertMetastoreInsertDir 设置为 false。

自 Spark 3.3 起,类似 round 函数的返回类型的精度已被修复。在使用先前版本创建的视图时,这可能导致 Spark 抛出 CANNOT_UP_CAST_DATATYPE 错误类的 AnalysisException。在这种情况下,您需要使用较新的 Spark 版本通过 ALTER VIEW ASCREATE OR REPLACE VIEW AS 重新创建视图。

自 Spark 3.3 起,unbase64 函数对格式错误的 str 输入抛出错误。使用 try_to_binary(<str>, 'base64') 可以容忍格式错误的输入并返回 NULL。在 Spark 3.2 及更早版本中,unbase64 函数对格式错误的 str 输入返回尽力而为的结果。

自 Spark 3.3 起,当读取不是由 Spark 生成的 Parquet 文件时,在模式推断期间,带有注解 isAdjustedToUTC = false 的 Parquet 时间戳列被推断为 TIMESTAMP_NTZ 类型。在 Spark 3.2 及更早版本中,这些列被推断为 TIMESTAMP 类型。要恢复 Spark 3.3 之前的行为,您可以将 spark.sql.parquet.inferTimestampNTZ.enabled 设置为 false。

自 Spark 3.3.1 和 3.2.3 起,对于 SELECT ... GROUP BY a GROUPING SETS (b) 风格的 SQL 语句,grouping__id 返回的值与 Apache Spark 3.2.0、3.2.1、3.2.2 和 3.3.0 不同。它基于用户给定的 group-by 表达式加上分组集列进行计算。要恢复 3.3.1 和 3.2.3 之前的行为,您可以设置 spark.sql.legacy.groupingIdWithAppendedUserGroupBy。有关详细信息,请参阅 SPARK-40218 和 SPARK-40562。

从 Spark SQL 3.1 升级到 3.2

自 Spark 3.2 起,如果路径包含空格,ADD FILE/JAR/ARCHIVE 命令要求每个路径用 "' 括起来。

自 Spark 3.2 起,所有支持的 JDBC 方言对 ROWID 使用 StringType。在 Spark 3.1 或更早版本中,Oracle 方言使用 StringType,其他方言使用 LongType

在 Spark 3.2 中,PostgreSQL JDBC 方言对 MONEY 使用 StringType,并且由于 PostgreSQL 的 JDBC 驱动程序无法正确处理这些类型,不支持 MONEY[]。在 Spark 3.1 或更早版本中,分别使用 DoubleTypeDoubleTypeArrayType

在 Spark 3.2 中,spark.sql.adaptive.enabled 默认启用。要恢复 Spark 3.2 之前的行为,您可以将 spark.sql.adaptive.enabled 设置为 false。

在 Spark 3.2 中,以下元字符在 show() 操作中被转义。在 Spark 3.1 或更早版本中,以下元字符按原样输出。

  • \n (换行)
  • \r (回车)
  • \t (水平制表符)
  • \f (换页)
  • \b (退格)
  • \u000B (垂直制表符)
  • \u0007 (响铃)

在 Spark 3.2 中,对于来自 Hive 外部表的表,当目标分区已存在时,ALTER TABLE .. RENAME TO PARTITION 抛出 PartitionAlreadyExistsException 而不是 AnalysisException

在 Spark 3.2 中,对于无 serde 模式,脚本转换的默认字段分隔符是 \u0001;对于 Hive serde 模式,当用户指定 serde 时,serde 属性 field.delim\t。在 Spark 3.1 或更早版本中,默认字段分隔符是 \t;对于 Hive serde 模式,当用户指定 serde 时,serde 属性 field.delim\u0001

在 Spark 3.2 中,生成列别名时,自动生成的 Cast(例如由类型强制转换规则添加的)将被剥离。例如,sql("SELECT floor(1)").columns 将是 FLOOR(1) 而不是 FLOOR(CAST(1 AS DOUBLE))

在 Spark 3.2 中,SHOW TABLES 的输出模式变为 namespace: string, tableName: string, isTemporary: boolean。在 Spark 3.1 或更早版本中,对于内置目录,namespace 字段名为 database,对于 v2 目录,没有 isTemporary 字段。要恢复内置目录的旧模式,您可以将 spark.sql.legacy.keepCommandOutputSchema 设置为 true。

在 Spark 3.2 中,SHOW TABLE EXTENDED 的输出模式变为 namespace: string, tableName: string, isTemporary: boolean, information: string。在 Spark 3.1 或更早版本中,对于内置目录,namespace 字段名为 database,对于 v2 目录没有变化。要恢复内置目录的旧模式,您可以将 spark.sql.legacy.keepCommandOutputSchema 设置为 true。

在 Spark 3.2 中,无论您是否指定表属性键,SHOW TBLPROPERTIES 的输出模式都是 key: string, value: string。在 Spark 3.1 及更早版本中,当您指定表属性键时,SHOW TBLPROPERTIES 的输出模式是 value: string。要恢复内置目录的旧模式,您可以将 spark.sql.legacy.keepCommandOutputSchema 设置为 true。

在 Spark 3.2 中,DESCRIBE NAMESPACE 的输出模式变为 info_name: string, info_value: string。在 Spark 3.1 或更早版本中,对于内置目录,info_name 字段名为 database_description_iteminfo_value 字段名为 database_description_value。要恢复内置目录的旧模式,您可以将 spark.sql.legacy.keepCommandOutputSchema 设置为 true。

在 Spark 3.2 中,表刷新会清除表的缓存数据以及其所有依赖项(如视图)的缓存数据,同时保持依赖项已缓存。以下命令执行表刷新:

  • ALTER TABLE .. ADD PARTITION
  • ALTER TABLE .. RENAME PARTITION
  • ALTER TABLE .. DROP PARTITION
  • ALTER TABLE .. RECOVER PARTITIONS
  • MSCK REPAIR TABLE
  • LOAD DATA
  • REFRESH TABLE
  • TRUNCATE TABLE
    以及方法 spark.catalog.refreshTable。在 Spark 3.1 及更早版本中,表刷新会使依赖项未缓存。

在 Spark 3.2 中,使用 count(tblName.*) 被阻止以避免产生歧义的结果。因为如果存在任何 null 值,count(*)count(tblName.*) 将输出不同的结果。要恢复 Spark 3.2 之前的行为,您可以将 spark.sql.legacy.allowStarWithSingleTableIdentifierInCount 设置为 true。

在 Spark 3.2 中,我们支持在 INSERTADD/DROP/RENAME PARTITION 的分区规范中使用类型化字面量。例如,ADD PARTITION(dt = date'2020-01-01') 添加一个日期值为 2020-01-01 的分区。在 Spark 3.1 及更早版本中,分区值将被解析为字符串值 date '2020-01-01',这是一个非法的日期值,我们最终添加一个值为 null 的分区。

在 Spark 3.2 中,DataFrameNaFunctions.replace() 不再对输入列名使用精确字符串匹配,以匹配 SQL 语法并支持限定列名。名称中包含点(非嵌套)的输入列名需要用反引号 ` 转义。现在,如果在数据框模式中找不到列,它会抛出 AnalysisException。如果输入列名是嵌套列,它也会抛出 IllegalArgumentException。在 Spark 3.1 及更早版本中,它会忽略无效的输入列名和嵌套列名。

在 Spark 3.2 中,日期减法表达式如 date1 - date2 返回 DayTimeIntervalType 的值。在 Spark 3.1 及更早版本中,返回的类型是 CalendarIntervalType。要恢复 Spark 3.2 之前的行为,您可以将 spark.sql.legacy.interval.enabled 设置为 true。

在 Spark 3.2 中,时间戳减法表达式如 timestamp '2021-03-31 23:48:00' - timestamp '2021-01-01 00:00:00' 返回 DayTimeIntervalType 的值。在 Spark 3.1 及更早版本中,相同表达式的类型是 CalendarIntervalType。要恢复 Spark 3.2 之前的行为,您可以将 spark.sql.legacy.interval.enabled 设置为 true。

在 Spark 3.2 中,CREATE TABLE .. LIKE .. 命令不能使用保留属性。您需要使用特定的子句来指定它们,例如,CREATE TABLE test1 LIKE test LOCATION 'some path'。您可以将 spark.sql.legacy.notReserveProperties 设置为 true 以忽略 ParseException,在这种情况下,这些属性将被静默移除,例如:TBLPROPERTIES('owner'='yao') 将没有效果。在 Spark 3.1 及以下版本中,保留属性可以在 CREATE TABLE .. LIKE .. 命令中使用,但没有副作用,例如,TBLPROPERTIES('location'='/tmp') 不会更改表的位置,而只是创建一个无头属性,就像 ‘a’=‘b’ 一样。

在 Spark 3.2 中,TRANSFORM 运算符不支持输入中的别名。在 Spark 3.1 及更早版本中,我们可以编写像 SELECT TRANSFORM(a AS c1, b AS c2) USING 'cat' FROM TBL 这样的脚本转换。

在 Spark 3.2 中,TRANSFORM 运算符支持没有 Hive SerDe 的 ArrayType/MapType/StructType,在这种模式下,我们使用 StructsToJsonArrayType/MapType/StructType 列转换为 STRING,并使用 JsonToStructsSTRING 解析为 ArrayType/MapType/StructType。在 Spark 3.1 中,Spark 仅支持将 ArrayType/MapType/StructType 列视为 STRING,但不支持将 STRING 解析为 ArrayType/MapType/StructType 输出列。

在 Spark 3.2 中,像 INTERVAL '1-1' YEAR TO MONTH 这样的单位到单位间隔字面量和像 INTERVAL '3' DAYS '1' HOUR 这样的单位列表间隔字面量被转换为 ANSI 间隔类型:YearMonthIntervalTypeDayTimeIntervalType。在 Spark 3.1 及更早版本中,此类间隔字面量被转换为 CalendarIntervalType。要恢复 Spark 3.2 之前的行为,您可以将 spark.sql.legacy.interval.enabled 设置为 true。

在 Spark 3.2 中,单位列表间隔字面量不能混合年-月字段(YEARMONTH)和日-时字段(WEEK, DAY, …, MICROSECOND)。例如,INTERVAL 1 month 1 hour 在 Spark 3.2 中无效。在 Spark 3.1 及更早版本中,没有这样的限制,字面量返回 CalendarIntervalType 的值。要恢复 Spark 3.2 之前的行为,您可以将 spark.sql.legacy.interval.enabled 设置为 true。

在 Spark 3.2 中,Spark 支持 DayTimeIntervalTypeYearMonthIntervalType 作为 Hive SERDE 模式下 TRANSFORM 子句的输入和输出,当这两种类型用作输入时,Hive SERDE 模式和 ROW FORMAT DELIMITED 模式的行为不同。在 Hive SERDE 模式下,DayTimeIntervalType 列被转换为 HiveIntervalDayTime,其字符串格式为 [-]?d h:m:s.n,但在 ROW FORMAT DELIMITED 模式下,格式为 INTERVAL '[-]?d h:m:s.n' DAY TO TIME。在 Hive SERDE 模式下,YearMonthIntervalType 列被转换为 HiveIntervalYearMonth,其字符串格式为 [-]?y-m,但在 ROW FORMAT DELIMITED 模式下,格式为 INTERVAL '[-]?y-m' YEAR TO MONTH

在 Spark 3.2 中,对于浮点类型,hash(0) == hash(-0)。之前,生成的值不同。

在 Spark 3.2 中,带有非空 LOCATIONCREATE TABLE AS SELECT 将抛出 AnalysisException。要恢复 Spark 3.2 之前的行为,您可以将 spark.sql.legacy.allowNonEmptyLocationInCTAS 设置为 true。

在 Spark 3.2 中,特殊日期时间值如 epochtodayyesterdaytomorrownow 仅在类型化字面量或可折叠字符串的强制转换中受支持,例如,select timestamp'now'select cast('today' as date)。在 Spark 3.1 和 3.0 中,此类特殊值在字符串到日期/时间戳的任何强制转换中都受支持。要在 Spark 3.1 和 3.0 中将这些特殊值保留为日期/时间戳,您应该手动替换它们,例如 if (c in ('now', 'today'), current_date(), cast(c as date))

在 Spark 3.2 中,FloatType 在 MySQL 中映射到 FLOAT。在此之前,它被映射到 REAL,在 MySQL 中默认是 DOUBLE PRECISION 的同义词。

在 Spark 3.2 中,由 DataFrameWriter 触发的查询执行在发送到 QueryExecutionListener 时始终命名为 command。在 Spark 3.1 及更早版本中,名称是 saveinsertIntosaveAsTable 之一。

在 Spark 3.2 中,将 allowMissingColumns 设置为 true 的 Dataset.unionByName 会将缺失的嵌套字段添加到结构体的末尾。在 Spark 3.1 中,嵌套结构字段按字母顺序排序。

在 Spark 3.2 中,如果输入查询输出列包含自动生成的别名,创建/更改视图将失败。这对于确保查询输出列名在不同 Spark 版本之间稳定是必要的。要恢复 Spark 3.2 之前的行为,将 spark.sql.legacy.allowAutoGeneratedAliasForView 设置为 true。

在 Spark 3.2 中,仅包含日-时字段的日期 +/- 间隔,例如 date '2011-11-11' + interval 12 hours 返回时间戳。在 Spark 3.1 及更早版本中,相同的表达式返回日期。要恢复 Spark 3.2 之前的行为,您可以使用 cast 将时间戳转换为日期。

从 Spark SQL 3.0 升级到 3.1

在 Spark 3.1 中,统计聚合函数包括 stdstddevstddev_sampvariancevar_sampskewnesskurtosiscovar_sampcorr 在表达式求值期间发生除零时返回 NULL,而不是 Double.NaN,例如,当 stddev_samp 应用于单元素集时。在 Spark 3.0 及更早版本中,在这种情况下会返回 Double.NaN。要恢复 Spark 3.1 之前的行为,您可以将 spark.sql.legacy.statisticalAggregate 设置为 true。

在 Spark 3.1 中,grouping_id() 返回 long 值。在 Spark 3.0 及更早版本中,此函数返回 int 值。要恢复 Spark 3.1 之前的行为,您可以将 spark.sql.legacy.integerGroupingId 设置为 true。

在 Spark 3.1 中,SQL UI 数据采用格式化模式显示查询计划解释结果。要恢复 Spark 3.1 之前的行为,您可以将 spark.sql.ui.explainMode 设置为 extended

在 Spark 3.1 中,如果指定的日期时间模式无效,from_unixtimeunix_timestampto_unix_timestampto_timestampto_date 将失败。在 Spark 3.0 或更早版本中,它们返回 NULL。

在 Spark 3.1 中,如果 Parquet、ORC、Avro 和 JSON 数据源在顶级列以及嵌套结构中检测到重复名称,它们会抛出异常 org.apache.spark.sql.AnalysisException: Found duplicate column(s) in the data schema。数据源在检测列名重复时会考虑 SQL 配置 spark.sql.caseSensitive

在 Spark 3.1 中,在将结构体和映射转换为字符串时,它们被 {} 括号括起来。例如,show() 操作和 CAST 表达式使用此类括号。在 Spark 3.0 及更早版本中,使用 [] 括号用于相同目的。要恢复 Spark 3.1 之前的行为,您可以将 spark.sql.legacy.castComplexTypesToString.enabled 设置为 true。

在 Spark 3.1 中,在将结构体、数组和映射转换为字符串时,它们的 NULL 元素被转换为 “null”。在 Spark 3.0 或更早版本中,NULL 元素被转换为空字符串。要恢复 Spark 3.1 之前的行为,您可以将 spark.sql.legacy.castComplexTypesToString.enabled 设置为 true。

在 Spark 3.1 中,当 spark.sql.ansi.enabled 为 false 时,如果十进制类型列的和溢出,Spark 始终返回 null。在 Spark 3.0 或更早版本中,在这种情况下,十进制类型列的和可能返回 null 或不正确的结果,甚至在运行时失败(取决于实际查询计划执行)。

在 Spark 3.1 中,当以下方法使用路径参数调用时,path 选项不能共存:DataFrameReader.load()DataFrameWriter.save()DataStreamReader.load()DataStreamWriter.start()。此外,对于 DataFrameReader.load()paths 选项不能共存。例如,spark.read.format("csv").option("path", "/tmp").load("/tmp2")spark.read.option("path", "/tmp").csv("/tmp2") 将抛出 org.apache.spark.sql.AnalysisException。在 Spark 3.0 及以下版本中,如果向上述方法传递一个路径参数,则 path 选项会被覆盖;如果向 DataFrameReader.load() 传递多个路径参数,则 path 选项会被添加到整体路径中。要恢复 Spark 3.1 之前的行为,您可以将 spark.sql.legacy.pathOptionBehavior.enabled 设置为 true。

在 Spark 3.1 中,不完整的间隔字面量(例如 INTERVAL '1'INTERVAL '1 DAY 2',这些是无效的)会返回 IllegalArgumentException。在 Spark 3.0 中,这些字面量会导致 NULL。

在 Spark 3.1 中,我们移除了内置的 Hive 1.2。您需要将自定义 SerDes 迁移到 Hive 2.3。有关更多详细信息,请参阅 HIVE-15167。

在 Spark 3.1 中,如果时间戳在 1900-01-01 00:00:00Z 之前,并且作为 INT96 类型加载(保存),则从/向 parquet 文件加载和保存时间戳会失败。在 Spark 3.0 中,操作不会失败,但由于从/向儒略历到/从公历的重新基准调整,可能会导致输入时间戳偏移。要恢复 Spark 3.1 之前的行为,您可以将 spark.sql.legacy.parquet.int96RebaseModeInRead 和/或 spark.sql.legacy.parquet.int96RebaseModeInWrite 设置为 LEGACY

在 Spark 3.1 中,schema_of_jsonschema_of_csv 函数以 SQL 格式返回模式,其中字段名被引用。在 Spark 3.0 中,函数返回一个没有字段引用且为小写的目录字符串。

在 Spark 3.1 中,刷新表将触发所有引用该表的其他缓存的未缓存操作,即使表本身未缓存。在 Spark 3.0 中,仅当表本身已缓存时才会触发该操作。

在 Spark 3.1 中,创建或更改永久视图将捕获运行时 SQL 配置并将其存储为视图属性。这些配置将在视图解析的解析和分析阶段应用。要恢复 Spark 3.1 之前的行为,您可以将 spark.sql.legacy.useCurrentConfigsForView 设置为 true。

在 Spark 3.1 中,临时视图将与永久视图具有相同的行为,即捕获并存储运行时 SQL 配置、SQL 文本、目录和命名空间。捕获的视图属性将在视图解析的解析和分析阶段应用。要恢复 Spark 3.1 之前的行为,您可以将 spark.sql.legacy.storeAnalyzedPlanForView 设置为 true。

在 Spark 3.1 中,通过 CACHE TABLE ... AS SELECT 创建的临时视图也将具有与永久视图相同的行为。特别是,当临时视图被删除时,Spark 将使所有其缓存依赖项以及临时视图本身的缓存无效。这与 Spark 3.0 及以下版本不同,后者仅执行后者。要恢复之前的行为,您可以将 spark.sql.legacy.storeAnalyzedPlanForView 设置为 true。

自 Spark 3.1 起,表模式中支持 CHAR/CHARACTERVARCHAR 类型。表扫描/插入将遵循 char/varchar 语义。如果在表模式之外的地方使用 char/varchar,将抛出异常(CAST 是一个例外,它像以前一样简单地将 char/varchar 视为字符串)。要恢复 Spark 3.1 之前的行为,即将它们视为 STRING 类型并忽略长度参数,例如 CHAR(4),您可以将 spark.sql.legacy.charVarcharAsString 设置为 true。

在 Spark 3.1 中,对于来自 Hive 外部目录的表,在以下情况下,AnalysisException 被其子类替换:

  • 如果新分区已存在,ALTER TABLE .. ADD PARTITION 抛出 PartitionsAlreadyExistException
  • 对于不存在的分区,ALTER TABLE .. DROP PARTITION 抛出 NoSuchPartitionsException

从 Spark SQL 3.0.1 升级到 3.0.2

在 Spark 3.0.2 中,对于来自 Hive 外部目录的表,在以下情况下,AnalysisException 被其子类替换:

  • 如果新分区已存在,ALTER TABLE .. ADD PARTITION 抛出 PartitionsAlreadyExistException
  • 对于不存在的分区,ALTER TABLE .. DROP PARTITION 抛出 NoSuchPartitionsException

在 Spark 3.0.2 中,PARTITION(col=null) 在分区规范中始终被解析为 null 字面量。在 Spark 3.0.1 或更早版本中,如果分区列是字符串类型,它被解析为其文本表示的字符串字面量,例如字符串 “null”。要恢复旧行为,您可以将 spark.sql.legacy.parseNullPartitionSpecAsStringLiteral 设置为 true。

在 Spark 3.0.2 中,SHOW DATABASES 的输出模式变为 namespace: string。在 Spark 3.0.1 及更早版本中,模式是 databaseName: string。自 Spark 3.0.2 起,您可以通过将 spark.sql.legacy.keepCommandOutputSchema 设置为 true 来恢复旧模式。

从 Spark SQL 3.0 升级到 3.0.1

在 Spark 3.0 中,如果字符串值与 JSON 选项 timestampFormat 定义的模式匹配,JSON 数据源和 JSON 函数 schema_of_json 会从字符串值推断 TimestampType。自版本 3.0.1 起,时间戳类型推断默认禁用。将 JSON 选项 inferTimestamp 设置为 true 以启用此类类型推断。

在 Spark 3.0 中,当将字符串强制转换为整数类型(tinyintsmallintintbigint)、日期时间类型(datetimestampinterval)和布尔类型时,前导和尾随字符(<= ASCII 32)将被修剪。例如,cast('\b1\b' as int) 结果为 1。自 Spark 3.0.1 起,仅修剪前导和尾随的空白 ASCII 字符。例如,cast('\t1\t' as int) 结果为 1,但 cast('\b1\b' as int) 结果为 NULL。

从 Spark SQL 2.4 升级到 3.0

Dataset/DataFrame API

在 Spark 3.0 中,Dataset 和 DataFrame API unionAll 不再被弃用。它是 union 的别名。

在 Spark 2.4 及以下版本中,如果键是非结构类型,例如 int、string、array 等,Dataset.groupByKey 的结果是一个分组数据集,其键属性错误地命名为 “value”。这违反直觉,并使聚合查询的模式出乎意料。例如,ds.groupByKey(...).count() 的模式是 (value, count)。自 Spark 3.0 起,我们将分组属性命名为 “key”。旧行为通过新添加的配置 spark.sql.legacy.dataset.nameNonStructGroupingKeyAsValue 保留,默认值为 false。

在 Spark 3.0 中,列元数据将始终在 API Column.nameColumn.as 中传播。在 Spark 2.4 及更早版本中,NamedExpression 的元数据在调用 API 时设置为新列的 explicitMetadata,即使底层的 NamedExpression 更改元数据,它也不会更改。要恢复 Spark 3.0 之前的行为,您可以使用带有显式元数据的 API as(alias: String, metadata: Metadata)

当将一个 Dataset 转换为另一个 Dataset 时,Spark 会将原始 Dataset 中的字段向上转换为目标 Dataset 中对应字段的类型。在 2.4 及更早版本中,此向上转换不是很严格,例如 Seq("str").toDS.as[Int] 失败,但 Seq("str").toDS.as[Boolean] 有效并在执行期间抛出 NPE。在 Spark 3.0 中,向上转换更严格,不允许将 String 转换为其他类型,例如 Seq("str").toDS.as[Boolean] 将在分析期间失败。要恢复 Spark 3.0 之前的行为,将 spark.sql.legacy.doLooseUpcast 设置为 true。

DDL 语句

在 Spark 3.0 中,当将值插入到具有不同数据类型的表列时,根据 ANSI SQL 标准执行类型强制转换。某些不合理的类型转换(例如将字符串转换为 int 和将 double 转换为 boolean)被禁止。如果值超出列的数据类型范围,则抛出运行时异常。在 Spark 2.4 及以下版本中,只要表插入期间的类型转换是有效的 Cast,就允许进行。当将超出范围的值插入整数字段时,插入值的低阶位(与 Java/Scala 数字类型强制转换相同)。例如,如果将 257 插入 byte 类型的字段,则结果为 1。该行为由选项 spark.sql.storeAssignmentPolicy 控制,默认值为 “ANSI”。将选项设置为 “Legacy” 可恢复之前的行为。

ADD JAR 命令之前返回一个包含单个值 0 的结果集。现在它返回一个空结果集。

Spark 2.4 及以下版本:即使指定的键用于 SparkConf 条目并且由于命令不更新 SparkConf 而无效,SET 命令也能正常工作而没有任何警告,但此行为可能会使用户困惑。在 3.0 中,如果使用了 SparkConf 键,命令将失败。您可以通过将 spark.sql.legacy.setCommandRejectsSparkCoreConfs 设置为 false 来禁用此类检查。

刷新缓存表将触发表未缓存操作,然后触发表缓存(惰性)操作。在 Spark 2.4 及以下版本中,缓存名称和存储级别在未缓存操作之前未保留。因此,缓存名称和存储级别可能会意外更改。在 Spark 3.0 中,首先保留缓存名称和存储级别以进行缓存重新创建。这有助于在表刷新时保持一致的缓存行为。

在 Spark 3.0 中,以下属性成为保留属性;如果您在 CREATE DATABASE ... WITH DBPROPERTIESALTER TABLE ... SET TBLPROPERTIES 等地方指定保留属性,命令将失败。您需要使用特定的子句来指定它们,例如 CREATE DATABASE test COMMENT 'any comment' LOCATION 'some path'。您可以将 spark.sql.legacy.notReserveProperties 设置为 true 以忽略 ParseException,在这种情况下,这些属性将被静默移除,例如:SET DBPROPERTIES('location'='/tmp') 将没有效果。在 Spark 2.4 及以下版本中,这些属性既不是保留属性也没有副作用,例如 SET DBPROPERTIES('location'='/tmp') 不会更改数据库的位置,而只是创建一个无头属性,就像 ‘a’=‘b’ 一样。

属性(区分大小写)数据库保留表保留备注
provider对于表,使用 USING 子句指定它。一旦设置,就无法更改。
location对于数据库和表,使用 LOCATION 子句指定它。
owner对于数据库和表,它由运行 spark 并创建表的用户确定。

在 Spark 3.0 中,您可以使用 ADD FILE 来添加文件目录。之前您只能使用此命令添加单个文件。要恢复早期版本的行为,将 spark.sql.legacy.addSingleFileInAddFile 设置为 true。

在 Spark 3.0 中,如果表不存在,SHOW TBLPROPERTIES 抛出 AnalysisException。在 Spark 2.4 及以下版本中,此场景导致 NoSuchTableException

在 Spark 3.0 中,SHOW CREATE TABLE table_identifier 始终返回 Spark DDL,即使给定表是 Hive SerDe 表。要生成 Hive DDL,请改用 SHOW CREATE TABLE table_identifier AS SERDE 命令。

在 Spark 3.0 中,非 Hive-Serde 表中不允许使用 CHAR 类型的列,如果检测到 CHAR 类型,CREATE/ALTER TABLE 命令将失败。请改用 STRING 类型。在 Spark 2.4 及以下版本中,CHAR 类型被视为 STRING 类型,长度参数被简单忽略。

UDF 和内置函数

在 Spark 3.0 中,date_adddate_sub 函数仅接受 int、smallint、tinyint 作为第二个参数;小数和非字面量字符串不再有效,例如:date_add(cast('1964-05-23' as date), '12.34') 导致 AnalysisException。请注意,字符串字面量仍然允许,但如果字符串内容不是有效的整数,Spark 会抛出 AnalysisException。在 Spark 2.4 及以下版本中,如果第二个参数是小数或字符串值,它被强制转换为 int 值,结果是日期值 1964-06-04。

在 Spark 3.0 中,函数 percentile_approx 及其别名 approx_percentile 仅接受范围在 [1, 2147483647] 内的整数值作为其第三个参数 accuracy,小数和字符串类型被禁止,例如,percentile_approx(10.0, 0.2, 1.8D) 导致 AnalysisException。在 Spark 2.4 及以下版本中,如果 accuracy 是小数或字符串值,它被强制转换为 int 值,percentile_approx(10.0, 0.2, 1.8D) 被操作为 percentile_approx(10.0, 0.2, 1),结果为 10.0。

在 Spark 3.0 中,当哈希表达式应用于 MapType 的元素时,会抛出分析异常。要恢复 Spark 3.0 之前的行为,将 spark.sql.legacy.allowHashOnMapType 设置为 true。

在 Spark 3.0 中,当 array/map 函数在没有参数的情况下调用时,它返回一个元素类型为 NullType 的空集合。在 Spark 2.4 及以下版本中,它返回一个元素类型为 StringType 的空集合。要恢复 Spark 3.0 之前的行为,您可以将 spark.sql.legacy.createEmptyCollectionUsingStringType 设置为 true。

在 Spark 3.0 中,from_json 函数支持两种模式 - PERMISSIVEFAILFAST。可以通过 mode 选项设置模式。默认模式变为 PERMISSIVE。在先前版本中,from_json 的行为不符合 PERMISSIVEFAILFAST,尤其是在处理格式错误的 JSON 记录时。例如,带有模式 a INT 的 JSON 字符串 {"a" 1} 被先前版本转换为 null,但 Spark 3.0 将其转换为 Row(null)

在 Spark 2.4 及以下版本中,您可以通过内置函数(如 CreateMapMapFromArrays 等)创建具有映射类型键的映射值。在 Spark 3.0 中,不允许使用这些内置函数创建具有映射类型键的映射值。用户可以使用 map_entries 函数将映射转换为 array<struct<key, value>> 作为变通方法。此外,用户仍然可以从数据源或 Java/Scala 集合读取具有映射类型键的映射值,但不鼓励这样做。

在 Spark 2.4 及以下版本中,您可以通过内置函数(如 CreateMapStringToMap 等)创建具有重复键的映射。具有重复键的映射的行为是未定义的,例如,映射查找尊重首先出现的重复键,Dataset.collect 仅保留最后出现的重复键,MapKeys 返回重复键等。在 Spark 3.0 中,当发现重复键时,Spark 抛出 RuntimeException。您可以将 spark.sql.mapKeyDedupPolicy 设置为 LAST_WIN 以使用最后获胜策略对映射键进行去重。用户仍然可以从不强制执行此操作的数据源(例如 Parquet)读取具有重复键的映射值,行为是未定义的。

在 Spark 3.0 中,默认不允许使用 org.apache.spark.sql.functions.udf(AnyRef, DataType)。建议移除返回类型参数以自动切换到类型化 Scala udf,或者将 spark.sql.legacy.allowUntypedScalaUDF 设置为 true 以继续使用它。在 Spark 2.4 及以下版本中,如果 org.apache.spark.sql.functions.udf(AnyRef, DataType) 获取一个带有基本类型参数的 Scala 闭包,当输入值为 null 时,返回的 UDF 返回 null。然而,在 Spark 3.0 中,如果输入值为 null,UDF 返回 Java 类型的默认值。例如,val f = udf((x: Int) => x, IntegerType),如果列 x 为 null,在 Spark 2.4 及以下版本中 f($"x") 返回 null,在 Spark 3.0 中返回 0。引入此行为更改是因为 Spark 3.0 默认使用 Scala 2.12 构建。

在 Spark 3.0 中,高阶函数 exists 遵循三值布尔逻辑,即,如果谓词返回任何 null 且未获得 true,则 exists 返回 null 而不是 false。例如,exists(array(1, null, 3), x -> x % 2 == 0) 为 null。可以通过将 spark.sql.legacy.followThreeValuedLogicInArrayExists 设置为 false 来恢复之前的行为。

在 Spark 3.0 中,如果原始日期是月份的最后一天,add_months 函数不会将结果日期调整到月份的最后一天。例如,select add_months(DATE'2019-02-28', 1) 结果为 2019-03-28。在 Spark 2.4 及以下版本中,当原始日期是月份的最后一天时,结果日期会被调整。例如,向 2019-02-28 添加一个月结果为 2019-03-31。

在 Spark 2.4 及以下版本中,current_timestamp 函数仅返回毫秒分辨率的时间戳。在 Spark 3.0 中,如果系统上可用的底层时钟提供微秒分辨率,该函数可以返回微秒分辨率的结果。

在 Spark 3.0 中,0 参数 Java UDF 在执行器端与其他 UDF 相同地执行。在 Spark 2.4 及以下版本中,单独的 0 参数 Java UDF 在驱动程序端执行,结果传播到执行器,这在某些情况下可能性能更高,但在某些情况下会导致不一致和正确性问题。

java.lang.Mathloglog1pexpexpm1pow 的结果可能因平台而异。在 Spark 3.0 中,等效 SQL 函数(包括相关的 SQL 函数如 LOG10)的返回值与 java.lang.StrictMath 一致。在几乎所有情况下,这对返回值没有影响,差异非常小,但在某些情况下可能不完全匹配 x86 平台上的 java.lang.Math,例如 log(3.0),其值在 Math.log()StrictMath.log() 之间变化。

在 Spark 3.0 中,当将字面量如 ‘Infinity’、‘+Infinity’、‘-Infinity’、‘NaN’、‘Inf’、‘+Inf’、‘-Inf’ 强制转换为 DoubleFloat 类型时,cast 函数以不区分大小写的方式处理这些字面量,以确保与其他数据库系统更好地兼容。此行为更改如下表所示:

操作Spark 3.0 之前的结果Spark 3.0 的结果
CAST('infinity' AS DOUBLE)NULLDouble.PositiveInfinity
CAST('+infinity' AS DOUBLE)NULLDouble.PositiveInfinity
CAST('inf' AS DOUBLE)NULLDouble.PositiveInfinity
CAST('+inf' AS DOUBLE)NULLDouble.PositiveInfinity
CAST('-infinity' AS DOUBLE)NULLDouble.NegativeInfinity
CAST('-inf' AS DOUBLE)NULLDouble.NegativeInfinity
CAST('infinity' AS FLOAT)NULLFloat.PositiveInfinity
CAST('+infinity' AS FLOAT)NULLFloat.PositiveInfinity
CAST('inf' AS FLOAT)NULLFloat.PositiveInfinity
CAST('+inf' AS FLOAT)NULLFloat.PositiveInfinity
CAST('-infinity' AS FLOAT)NULLFloat.NegativeInfinity
CAST('-inf' AS FLOAT)NULLFloat.NegativeInfinity
CAST('nan' AS DOUBLE)NULLDouble.NaN
CAST('nan' AS FLOAT)NULLFloat.NaN

在 Spark 3.0 中,当将间隔值强制转换为字符串类型时,没有 “interval” 前缀,例如 1 days 2 hours。在 Spark 2.4 及以下版本中,字符串包含 “interval” 前缀,如 interval 1 days 2 hours

在 Spark 3.0 中,当将字符串值强制转换为整数类型(tinyintsmallintintbigint)、日期时间类型(datetimestampinterval)和布尔类型时,在转换为这些类型值之前,前导和尾随的空白字符(<= ASCII 32)将被修剪,例如,cast(' 1\t' as int) 结果为 1,cast(' 1\t' as boolean) 结果为 true,cast('2019-10-10\t' as date) 结果为日期值 2019-10-10。在 Spark 2.4 及以下版本中,当将字符串强制转换为整数和布尔值时,它不会修剪两端的空白字符;前述结果为 null,而对于日期时间,仅删除尾随空格(= ASCII 32)。

查询引擎

在 Spark 2.4 及以下版本中,SQL 查询如 FROM <table>FROM <table> UNION ALL FROM <table> 偶然受支持。在 hive 风格的 FROM <table> SELECT <expr> 中,SELECT 子句不可忽略。Hive 和 Presto 都不支持此语法。这些查询在 Spark 3.0 中被视为无效。

在 Spark 3.0 中,间隔字面量语法不再允许多个 from-to 单位。例如,SELECT INTERVAL '1-1' YEAR TO MONTH '2-2' YEAR TO MONTH' 抛出解析器异常。

在 Spark 3.0 中,用科学记数法书写的数字(例如,1E2)将被解析为 Double。在 Spark 2.4 及以下版本中,它们被解析为 Decimal。要恢复 Spark 3.0 之前的行为,您可以将 spark.sql.legacy.exponentLiteralAsDecimal.enabled 设置为 true。

在 Spark 3.0 中,日时间间隔字符串根据指定的 from 和 to 边界转换为间隔。如果输入字符串与指定边界定义的模式不匹配,则抛出 ParseException 异常。例如,INTERVAL '2 10:20' HOUR TO MINUTE 引发异常,因为期望的格式是 [+|-]h[h]:[m]m。在 Spark 2.4 中,不考虑 from 边界,to 边界用于截断结果间隔。例如,所示示例中的日时间间隔字符串被转换为间隔 10 小时 20 分钟。要恢复 Spark 3.0 之前的行为,您可以将 spark.sql.legacy.fromDayTimeString.enabled 设置为 true。

在 Spark 3.0 中,默认不允许十进制的负刻度,例如,像 1E10BD 这样的字面量的数据类型是 DecimalType(11, 0)。在 Spark 2.4 及以下版本中,它是 DecimalType(2, -9)。要恢复 Spark 3.0 之前的行为,您可以将 spark.sql.legacy.allowNegativeScaleOfDecimal 设置为 true。

在 Spark 3.0 中,一元算术运算符 plus(+) 仅接受字符串、数字和间隔类型值作为输入。此外,带有整数字符串表示的 + 被强制转换为 double 值,例如,+'1' 返回 1.0。在 Spark 2.4 及以下版本中,此运算符被忽略。没有对其进行类型检查,因此,所有带有 + 前缀的类型值都是有效的,例如,+ array(1, 2) 是有效的,结果为 [1, 2]。此外,根本没有类型强制转换,例如,在 Spark 2.4 中,+'1' 的结果是字符串 1

在 Spark 3.0 中,如果 Dataset 查询包含由自连接引起的歧义列引用,则查询失败。典型示例:val df1 = ...; val df2 = df1.filter(...);,然后 df1.join(df2, df1("a") > df2("a")) 返回一个空结果,这非常令人困惑。这是因为 Spark 无法解析指向自连接表的 Dataset 列引用,并且 df1("a") 在 Spark 中与 df2("a") 完全相同。要恢复 Spark 3.0 之前的行为,您可以将 spark.sql.analyzer.failAmbiguousSelfJoin 设置为 false。

在 Spark 3.0 中,引入了 spark.sql.legacy.ctePrecedencePolicy 来控制嵌套 WITH 子句中名称冲突的行为。默认值为 EXCEPTION,Spark 会抛出 AnalysisException,它强制用户选择他们想要的特定替换顺序。如果设置为 CORRECTED(推荐),内部 CTE 定义优先于外部定义。例如,将配置设置为 CORRECTEDWITH t AS (SELECT 1), t2 AS (WITH t AS (SELECT 2) SELECT * FROM t) SELECT * FROM t2 返回 2,而将其设置为 LEGACY,结果为 1,这是 2.4 及以下版本的行为。

在 Spark 3.0 中,配置 spark.sql.crossJoin.enabled 成为内部配置,并且默认值为 true,因此默认情况下,Spark 不会对带有隐式交叉连接的 SQL 抛出异常。

在 Spark 2.4 及以下版本中,浮点数 -0.0 在语义上等于 0.0,但当在聚合分组键、窗口分区键和连接键中使用时,-0.00.0 被视为不同的值。在 Spark 3.0 中,此错误已修复。例如,Seq(-0.0, 0.0).toDF("d").groupBy("d").count() 在 Spark 3.0 中返回 [(0.0, 2)],而在 Spark 2.4 及以下版本中返回 [(0.0, 1), (-0.0, 1)]

在 Spark 2.4 及以下版本中,无效的时区 ID 会被静默忽略并替换为 GMT 时区,例如在 from_utc_timestamp 函数中。在 Spark 3.0 中,此类时区 ID 会被拒绝,Spark 会抛出 java.time.DateTimeException

在 Spark 3.0 中,在解析、格式化和转换日期和时间戳以及提取子组件(如年、日等)时,使用公历日历。Spark 3.0 使用基于 ISO 年表的 java.time 包中的 Java 8 API 类。在 Spark 2.4 及以下版本中,这些操作使用混合日历(儒略历 + 公历)执行。这些更改影响了 1582 年 10 月 15 日(公历)之前的日期结果,并影响以下 Spark 3.0 API:

  • 时间戳/日期字符串的解析/格式化:这影响 CSV/JSON 数据源以及当用户指定的模式用于解析和格式化时的 unix_timestampdate_formatto_unix_timestampfrom_unixtimeto_dateto_timestamp 函数。在 Spark 3.0 中,我们在日期时间模式格式化和解析中定义了自己的模式字符串,它通过 DateTimeFormatter 在底层实现。新实现对其输入执行严格检查。例如,如果模式是 yyyy-MM-dd,则无法解析时间戳 2015-07-22 10:00:00,因为解析器没有消耗整个输入。另一个例子是,输入 31/01/2015 00:00 不能被模式 dd/MM/yyyy hh:mm 解析,因为 hh 假设小时在 1-12 范围内。在 Spark 2.4 及以下版本中,使用 java.text.SimpleDateFormat 进行时间戳/日期字符串转换,支持的模式在 SimpleDateFormat 中描述。可以通过将 spark.sql.legacy.timeParserPolicy 设置为 LEGACY 来恢复旧行为。
  • weekofyearweekdaydayofweekdate_truncfrom_utc_timestampto_utc_timestampunix_timestamp 函数使用 java.time API 计算年的周数、周的日数以及从/向 UTC 时区的 TimestampType 值进行转换。
  • JDBC 选项 lowerBoundupperBound 的转换方式与将字符串强制转换为 TimestampType/DateType 值的方式相同。转换基于公历日历和 SQL 配置 spark.sql.session.timeZone 定义的时区。在 Spark 2.4 及以下版本中,转换基于混合日历(儒略历 + 公历)和默认系统时区。
  • 格式化 TIMESTAMPDATE 字面量
  • 从字符串创建类型化的 TIMESTAMPDATE 字面量:在 Spark 3.0 中,字符串到类型化 TIMESTAMP/DATE 字面量的转换通过强制转换为 TIMESTAMP/DATE 值来执行。例如,TIMESTAMP '2019-12-23 12:59:30' 在语义上等于 CAST('2019-12-23 12:59:30' AS TIMESTAMP)。当输入字符串不包含时区信息时,在这种情况下使用 SQL 配置 spark.sql.session.timeZone 中的时区。在 Spark 2.4 及以下版本中,转换基于 JVM 系统时区。默认时区的不同来源可能会更改类型化 TIMESTAMPDATE 字面量的行为。
  • 在 Spark 3.0 中,TIMESTAMP 字面量使用 SQL 配置 spark.sql.session.timeZone 转换为字符串。在 Spark 2.4 及以下版本中,转换使用 Java 虚拟机的默认时区。
  • 在 Spark 3.0 中,Spark 在与日期/时间戳的二进制比较中将 String 强制转换为 Date/Timestamp。可以通过将 spark.sql.legacy.typeCoercion.datetimeToString.enabled 设置为 true 来恢复将 Date/Timestamp 强制转换为 String 的先前行为。
  • 在 Spark 3.0 中,支持在从字符串到日期和时间戳的转换中使用特殊值。这些值只是符号简写,在读取时会转换为普通的日期或时间戳值。支持以下日期字符串值:
    • epoch [zoneId] - 1970-01-01
    • today [zoneId] - SQL 配置 spark.sql.session.timeZone 指定的时区中的当前日期
    • yesterday [zoneId] - 当前日期 - 1
    • tomorrow [zoneId] - 当前日期 + 1
    • now - 运行当前查询的日期。它与 today 具有相同的概念。
      例如,SELECT date 'tomorrow' - date 'yesterday'; 应该输出 2。以下是特殊的时间戳值:
    • epoch [zoneId] - 1970-01-01 00:00:00+00 (Unix 系统时间零)
    • today [zoneId] - 今天的午夜
    • yesterday [zoneId] - 昨天的午夜
    • tomorrow [zoneId] - 明天的午夜
    • now - 当前查询开始时间
      例如 SELECT timestamp 'tomorrow';

自 Spark 3.0 起,当使用 EXTRACT 表达式从日期/时间戳值中提取第二个字段时,结果将是一个 DecimalType(8, 6) 值,其中秒部分为 2 位数字,微秒精度的分数部分为 6 位数字。例如,extract(second from to_timestamp('2019-09-20 10:10:10.1')) 结果为 10.100000。在 Spark 2.4 及更早版本中,它返回一个 IntegerType 值,前述示例的结果为 10

在 Spark 3.0 中,日期时间模式字母 F 表示月份中对齐的周中日,它表示在一个周期内(周与月份开始对齐)的天数概念。在 Spark 2.4 及更早版本中,它表示月份中的周数,即周在固定的星期几开始的概念。例如,2020-07-30 是该月第一天之后的 30 天(4 周零 2 天),因此 date_format(date '2020-07-30', 'F') 在 Spark 3.0 中返回 2,但作为 Spark 2.x 中的周计数,它返回 5,因为它位于 2020 年 7 月的第 5 周,其中第一周是 2020-07-01 到 07-04。

在 Spark 3.0 中,Spark 将尝试在 CTAS 中使用内置数据源写入器而不是 Hive serde。仅当分别为 Parquet 和 ORC 格式启用 spark.sql.hive.convertMetastoreParquetspark.sql.hive.convertMetastoreOrc 时,此行为才有效。要恢复 Spark 3.0 之前的行为,您可以将 spark.sql.hive.convertMetastoreCtas 设置为 false。

在 Spark 3.0 中,Spark 将尝试使用内置数据源写入器而不是 Hive serde 来处理插入到使用 HiveSQL 语法创建的分区 ORC/Parquet 表中。仅当分别为 Parquet 和 ORC 格式启用 spark.sql.hive.convertMetastoreParquetspark.sql.hive.convertMetastoreOrc 时,此行为才有效。要恢复 Spark 3.0 之前的行为,您可以将 spark.sql.hive.convertInsertingPartitionedTable 设置为 false。

数据源
在 Spark 2.4 及以下版本中,当使用 Spark 原生数据源(parquet/orc)读取 Hive SerDe 表时,Spark 会推断实际文件模式并更新元存储中的表模式。在 Spark 3.0 中,Spark 不再推断模式。这不应给最终用户带来任何问题,但如果有问题,请将 spark.sql.hive.caseSensitiveInferenceMode 设置为 INFER_AND_SAVE

在 Spark 2.4 及以下版本中,如果分区列值无法强制转换为相应的用户提供模式,则将其转换为 null。在 3.0 中,分区列值使用用户提供的模式进行验证。如果验证失败,则抛出异常。您可以通过将 spark.sql.sources.validatePartitionColumns 设置为 false 来禁用此类验证。

在 Spark 3.0 中,如果文件或子目录在递归目录列表期间消失(即,由于并发文件删除或对象存储一致性问题,它们出现在中间列表中,但随后在递归目录列表的后续阶段无法读取或列出),则列表将失败并抛出异常,除非 spark.sql.files.ignoreMissingFiles 为 true(默认为 false)。在先前版本中,这些缺失的文件或子目录将被忽略。请注意,此行为更改仅适用于初始表文件列表(或在 REFRESH TABLE 期间),不适用于查询执行:净变化是 spark.sql.files.ignoreMissingFiles 现在在表文件列表/查询规划期间被遵守,而不仅仅是在查询执行时。

在 Spark 2.4 及以下版本中,JSON 数据源的解析器对于某些数据类型(如 IntegerType)将空字符串视为 null。对于 FloatTypeDoubleTypeDateTypeTimestampType,它在空字符串上失败并抛出异常。Spark 3.0 不允许空字符串,并将为除 StringTypeBinaryType 之外的数据类型抛出异常。可以通过将 spark.sql.legacy.json.allowEmptyString.enabled 设置为 true 来恢复允许空字符串的先前行为。

在 Spark 2.4 及以下版本中,当指定模式为 StructType 时,JSON 数据源和 JSON 函数(如 from_json)在 PERMISSIVE 模式下将错误的 JSON 记录转换为所有字段为 null 的行。在 Spark 3.0 中,如果某些 JSON 列值被成功解析并转换为所需类型,返回的行可以包含非 null 字段。

在 Spark 3.0 中,如果字符串值与 JSON 选项 timestampFormat 定义的模式匹配,JSON 数据源和 JSON 函数 schema_of_json 会从字符串值推断 TimestampType。将 JSON 选项 inferTimestamp 设置为 false 以禁用此类类型推断。

在 Spark 2.4 及以下版本中,CSV 数据源在 PERMISSIVE 模式下将格式错误的 CSV 字符串转换为所有字段为 null 的行。在 Spark 3.0 中,如果某些 CSV 列值被成功解析并转换为所需类型,返回的行可以包含非 null 字段。

在 Spark 3.0 中,当使用用户提供的模式写入 Avro 文件时,字段通过字段名称在催化剂模式和 Avro 模式之间匹配,而不是通过位置。

在 Spark 3.0 中,当使用用户提供的不可为空模式写入 Avro 文件时,即使催化剂模式可为空,Spark 仍然能够写入文件。但是,如果任何记录包含 null,Spark 会抛出运行时 NullPointerException

在 Spark 2.4 及以下版本中,当文件开头有 BOM 时,CSV 数据源可以自动检测输入文件的编码。例如,在多行模式(CSV 选项 multiLine 设置为 true)下,CSV 数据源可以识别 UTF-8、UTF-16BE、UTF-16LE、UTF-32BE 和 UTF-32LE。在 Spark 3.0 中,CSV 数据源读取通过 CSV 选项 encoding 指定的编码的输入文件,该选项的默认值为 UTF-8。这样,如果文件编码与通过 CSV 选项 encoding 指定的编码不匹配,Spark 会错误地加载文件。要解决此问题,用户应通过 CSV 选项 encoding 设置正确的编码,或将选项设置为 null,这将回退到 Spark 3.0 之前版本中的编码自动检测。

其他
在 Spark 2.4 版本中,当通过 cloneSession() 创建 Spark 会话时,新创建的 Spark 会话从其父 SparkContext 继承其配置,即使相同的配置可能在其父 Spark 会话中以不同的值存在。在 Spark 3.0 中,父 SparkSession 的配置比父 SparkContext 具有更高的优先级。您可以通过将 spark.sql.legacy.sessionInitWithConfigDefaults 设置为 true 来恢复旧行为。

在 Spark 3.0 中,如果在 Spark SQL 配置中找不到 hive.default.fileformat,则它会回退到 SparkContext 的 Hadoop 配置中存在的 hive-site.xml 文件。

在 Spark 3.0 中,我们为 spark-sql 接口的十进制数字填充尾随零以达到列的比例,例如:

查询Spark 2.4Spark 3.0
SELECT CAST(1 AS decimal(38, 18));11.000000000000000000

在 Spark 3.0 中,我们将内置的 Hive 从 1.2 升级到 2.3,这带来了以下影响:

  • 您可能需要根据要连接的 Hive 元存储版本设置 spark.sql.hive.metastore.versionspark.sql.hive.metastore.jars。例如:如果您的 Hive 元存储版本是 1.2.1,则将 spark.sql.hive.metastore.version 设置为 1.2.1,将 spark.sql.hive.metastore.jars 设置为 maven。
  • 您需要将自定义 SerDes 迁移到 Hive 2.3,或使用 hive-1.2 配置文件构建自己的 Spark。有关更多详细信息,请参阅 HIVE-15167。
  • 在 SQL 中使用 TRANSFORM 运算符进行脚本转换时,Hive 1.2 和 Hive 2.3 之间的十进制字符串表示可能不同,这取决于 hive 的行为。在 Hive 1.2 中,字符串表示省略尾随零。但在 Hive 2.3 中,如果需要,它总是填充到 18 位数字并带有尾随零。

从 Spark SQL 2.4.7 升级到 2.4.8

在 Spark 2.4.8 中,对于来自 Hive 外部目录的表,在以下情况下,AnalysisException 被其子类替换:

  • 如果新分区已存在,ALTER TABLE .. ADD PARTITION 抛出 PartitionsAlreadyExistException
  • 对于不存在的分区,ALTER TABLE .. DROP PARTITION 抛出 NoSuchPartitionsException

从 Spark SQL 2.4.5 升级到 2.4.6

在 Spark 2.4.6 中,RESET 命令不会将静态 SQL 配置值重置为默认值。它仅清除运行时 SQL 配置值。

从 Spark SQL 2.4.4 升级到 2.4.5

自 Spark 2.4.5 起,TRUNCATE TABLE 命令在重新创建表/分区路径期间尝试设置回原始权限和 ACL。要恢复早期版本的行为,将 spark.sql.truncateTable.ignorePermissionAcl.enabled 设置为 true。

自 Spark 2.4.5 起,添加了 spark.sql.legacy.mssqlserver.numericMapping.enabled 配置,以支持分别对 SMALLINTREAL JDBC 类型使用 IntegerTypeDoubleType 的旧 MsSQLServer 方言映射行为。要恢复 2.4.3 及更早版本的行为,将 spark.sql.legacy.mssqlserver.numericMapping.enabled 设置为 true。

从 Spark SQL 2.4.3 升级到 2.4.4

自 Spark 2.4.4 起,根据 MsSqlServer 指南,MsSQLServer JDBC 方言分别对 SMALLINTREAL 使用 ShortTypeFloatType。之前,使用 IntegerTypeDoubleType

从 Spark SQL 2.4 升级到 2.4.1

在 Spark 2.4.0 中,当 spark.executor.heartbeatInterval 的值指定时没有单位(如 “30” 而不是 “30s”),在代码的不同部分被不一致地解释为秒和毫秒。无单位的值现在被一致地解释为毫秒。设置像 “30” 这样的值的应用程序现在需要指定带单位的值,如 “30s”,以避免被解释为毫秒;否则,产生的极短间隔很可能会导致应用程序失败。

从 Spark SQL 2.3 升级到 2.4

在 Spark 2.3 及更早版本中,array_contains 函数的第二个参数被隐式提升为第一个数组类型参数的元素类型。这种类型提升可能是有损的,并可能导致 array_contains 函数返回错误结果。此问题在 2.4 中通过采用更安全的类型提升机制得到解决。这可能导致一些行为变化,如下表所示。

查询Spark 2.3 或更早Spark 2.4备注
SELECT array_contains(array(1), 1.34D);truefalse在 Spark 2.4 中,左右参数分别提升为 double 类型的数组类型和 double 类型。
SELECT array_contains(array(1), '1');true抛出 AnalysisException可以在参数中使用显式转换来避免异常。在 Spark 2.4 中,由于整数类型无法无损地提升为字符串类型,因此抛出 AnalysisException
SELECT array_contains(array(1), 'anystring');null抛出 AnalysisException可以在参数中使用显式转换来避免异常。在 Spark 2.4 中,由于整数类型无法无损地提升为字符串类型,因此抛出 AnalysisException

自 Spark 2.4 起,当子查询前的 IN 运算符前有结构体字段时,内部查询也必须包含结构体字段。在先前版本中,结构体的字段与内部查询的输出进行比较。例如,如果 a 是一个结构体 (a string, b int),在 Spark 2.4 中 a in (select (1 as a, 'a' as b) from range(1)) 是一个有效的查询,而 a in (select 1, 'a' from range(1)) 则无效。在先前版本中,情况相反。

在 2.2.1+ 和 2.3 版本中,如果 spark.sql.caseSensitive 设置为 true,则 CURRENT_DATECURRENT_TIMESTAMP 函数错误地变得区分大小写,并会解析为列(除非使用小写键入)。在 Spark 2.4 中,此问题已修复,这些函数不再区分大小写。

自 Spark 2.4 起,Spark 将根据 SQL 标准遵循优先级规则来评估查询中引用的集合操作。如果顺序未由括号指定,则集合操作从左到右执行,但所有 INTERSECT 操作在任何 UNIONEXCEPTMINUS 操作之前执行。将所有集合操作赋予同等优先级的旧行为通过新添加的配置 spark.sql.legacy.setopsPrecedence.enabled 保留,默认值为 false。当此属性设置为 true 时,spark 将按照查询中出现的顺序从左到右评估集合运算符,前提是没有通过使用括号强制显式排序。

自 Spark 2.4 起,当值为 1970 年 1 月 1 日时,Spark 将表描述列 Last Access 的值显示为 UNKNOWN

自 Spark 2.4 起,Spark 默认最大化使用向量化 ORC 读取器来读取 ORC 文件。为此,spark.sql.orc.implspark.sql.orc.filterPushdown 的默认值分别更改为 nativetrue。由原生 ORC 写入器创建的 ORC 文件无法被某些旧的 Apache Hive 版本读取。使用 spark.sql.orc.impl=hive 创建与 Hive 2.1.1 及更早版本共享的文件。

自 Spark 2.4 起,将空数据框写入目录会启动至少一个写入任务,即使物理上数据框没有分区。这引入了一个小的行为变化:对于像 Parquet 和 Orc 这样的自描述文件格式,Spark 在写入 0 分区数据框时会在目标目录中创建一个仅包含元数据的文件,以便用户稍后读取该目录时仍然可以进行模式推断。关于写入空数据框,新行为更合理且更一致。

自 Spark 2.4 起,UDF 参数中的表达式 ID 不会出现在列名中。例如,Spark 2.4 中的列名不是 UDF:f(col0 AS colA#28) 而是 UDF:f(col0 AS ``colA``)

自 Spark 2.4 起,不允许使用任何文件格式(parquet、orc、json、text、csv 等)写入具有空模式或嵌套空模式的数据框。尝试写入具有空模式的数据框时会抛出异常。

自 Spark 2.4 起,Spark 在将双方都提升为 TIMESTAMP 后比较 DATE 类型和 TIMESTAMP 类型。将 spark.sql.legacy.compareDateTimestampInTimestamp 设置为 false 可恢复先前行为。此选项将在 Spark 3.0 中移除。

自 Spark 2.4 起,不允许创建具有非空位置的托管表。尝试创建具有非空位置的托管表时会抛出异常。将 spark.sql.legacy.allowCreatingManagedTableUsingNonemptyLocation 设置为 true 可恢复先前行为。此选项将在 Spark 3.0 中移除。

自 Spark 2.4 起,不允许将托管表重命名为现有位置。尝试将托管表重命名为现有位置时会抛出异常。

自 Spark 2.4 起,类型强制转换规则可以自动将可变参数 SQL 函数(例如 IN/COALESCE)的参数类型提升为最宽的公共类型,无论输入参数的顺序如何。在先前 Spark 版本中,提升在某些特定顺序(例如 TimestampTypeIntegerTypeStringType)下可能失败并抛出异常。

自 Spark 2.4 起,Spark 除了传统的缓存失效机制外,还启用了非级联 SQL 缓存失效。非级联缓存失效机制允许用户移除缓存而不影响其依赖缓存。这种新的缓存失效机制用于要移除的缓存数据仍然有效的情况,例如,在 Dataset 上调用 unpersist() 或删除临时视图。这允许用户释放内存并同时保持所需缓存有效。

在 2.3 及更早版本中,Spark 默认转换 Parquet Hive 表,但忽略表属性,如 TBLPROPERTIES (parquet.compression 'NONE')。对于 ORC Hive 表属性,如 TBLPROPERTIES (orc.compress 'NONE'),在 spark.sql.hive.convertMetastoreOrc=true 时也会发生这种情况。自 Spark 2.4 起,Spark 在转换 Parquet/ORC Hive 表时尊重 Parquet/ORC 特定的表属性。例如,CREATE TABLE t(id int) STORED AS PARQUET TBLPROPERTIES (parquet.compression 'NONE') 在 Spark 2.3 的插入期间会生成 Snappy parquet 文件,而在 Spark 2.4 中,结果将是未压缩的 parquet 文件。

自 Spark 2.0 起,Spark 默认转换 Parquet Hive 表以获得更好的性能。自 Spark 2.4 起,Spark 也默认转换 ORC Hive 表。这意味着 Spark 默认使用自己的 ORC 支持而不是 Hive SerDe。例如,CREATE TABLE t(id int) STORED AS ORC 在 Spark 2.3 中由 Hive SerDe 处理,而在 Spark 2.4 中,它将被转换为 Spark 的 ORC 数据源表并应用 ORC 向量化。将 spark.sql.hive.convertMetastoreOrc 设置为 false 可恢复先前行为。

在 2.3 及更早版本中,如果行中至少有一个列值格式错误,则 CSV 行被视为格式错误。CSV 解析器在 DROPMALFORMED 模式下丢弃此类行,或在 FAILFAST 模式下输出错误。自 Spark 2.4 起,仅当 CSV 行包含从 CSV 数据源请求的格式错误的列值时,该行才被视为格式错误,其他值可以被忽略。例如,CSV 文件包含 “id,name” 标头和一行 “1234”。在 Spark 2.4 中,选择 id 列由包含一个列值 1234 的行组成,但在 Spark 2.3 及更早版本中,在 DROPMALFORMED 模式下为空。要恢复先前行为,将 spark.sql.csv.parser.columnPruning.enabled 设置为 false。

自 Spark 2.4 起,计算统计信息的文件列表默认并行完成。可以通过将 spark.sql.statistics.parallelFileListingInStatsComputation.enabled 设置为 False 来禁用此功能。

自 Spark 2.4 起,在统计计算期间计算表大小时,元数据文件(例如 Parquet 摘要文件)和临时文件不计为数据文件。

自 Spark 2.4 起,空字符串保存为带引号的空字符串 ""。在 2.3 及更早版本中,空字符串等于 null 值,并且不反映在保存的 CSV 文件中的任何字符。例如,行 "a", null, "", 1 被写为 a,,,1。自 Spark 2.4 起,同一行保存为 a,,"",1。要恢复先前行为,将 CSV 选项 emptyValue 设置为空(不带引号)字符串。

自 Spark 2.4 起,LOAD DATA 命令支持通配符 ?*,分别匹配任何一个字符和零个或多个字符。示例:LOAD DATA INPATH '/tmp/folder*/'LOAD DATA INPATH '/tmp/part-?'。特殊字符如空格现在也可以在路径中使用。示例:LOAD DATA INPATH '/tmp/folder name/'

在 Spark 2.3 及更早版本中,没有 GROUP BYHAVING 被当作 WHERE 处理。这意味着,SELECT 1 FROM range(10) HAVING true 被执行为 SELECT 1 FROM range(10) WHERE true 并返回 10 行。这违反了 SQL 标准,并在 Spark 2.4 中修复。自 Spark 2.4 起,没有 GROUP BYHAVING 被视为全局聚合,这意味着 SELECT 1 FROM range(10) HAVING true 将只返回一行。要恢复先前行为,将 spark.sql.legacy.parser.havingWithoutGroupByAsWhere 设置为 true。

在 2.3 及更早版本中,当从 Parquet 数据源表读取时,对于 Hive 元存储模式和 Parquet 模式中列名大小写不同的任何列,Spark 始终返回 null,无论 spark.sql.caseSensitive 设置为 true 还是 false。自 2.4 起,当 spark.sql.caseSensitive 设置为 false 时,Spark 在 Hive 元存储模式和 Parquet 模式之间执行不区分大小写的列名解析,因此即使列名大小写不同,Spark 也会返回相应的列值。如果存在歧义,即匹配到多个 Parquet 列,则会抛出异常。当 spark.sql.hive.convertMetastoreParquet 设置为 true 时,此更改也适用于 Parquet Hive 表。

从 Spark SQL 2.2 升级到 2.3

自 Spark 2.3 起,当引用的列仅包括内部损坏记录列(默认名为 _corrupt_record)时,不允许从原始 JSON/CSV 文件进行查询。例如,spark.read.schema(schema).json(file).filter($"_corrupt_record".isNotNull).count()spark.read.schema(schema).json(file).select("_corrupt_record").show()。相反,您可以缓存或保存解析结果,然后发送相同的查询。例如,val df = spark.read.schema(schema).json(file).cache(),然后 df.filter($"_corrupt_record".isNotNull).count()

percentile_approx 函数先前接受数字类型输入并输出 double 类型结果。现在它支持日期类型、时间戳类型和数字类型作为输入类型。结果类型也更改为与输入类型相同,这对于百分位数更合理。

自 Spark 2.3 起,Join/Filter 中位于第一个非确定性谓词之后的确定性谓词也会被下推/传递给子运算符(如果可能)。在先前 Spark 版本中,这些过滤器不符合谓词下推条件。

分区列推断先前为不同的推断类型找到不正确的公共类型,例如,先前它最终以 double 类型作为 double 类型和 date 类型的公共类型。现在它为此类冲突找到正确的公共类型。冲突解决遵循下表:

InputA \ InputBNullTypeIntegerTypeLongTypeDecimalType(38,0)*DoubleTypeDateTypeTimestampTypeStringType
NullTypeNullTypeIntegerTypeLongTypeDecimalType(38,0)DoubleTypeDateTypeTimestampTypeStringType
IntegerTypeIntegerTypeIntegerTypeLongTypeDecimalType(38,0)DoubleTypeStringTypeStringTypeStringType
LongTypeLongTypeLongTypeLongTypeDecimalType(38,0)StringTypeStringTypeStringTypeStringType
DecimalType(38,0)*DecimalType(38,0)DecimalType(38,0)DecimalType(38,0)DecimalType(38,0)StringTypeStringTypeStringTypeStringType
DoubleTypeDoubleTypeDoubleTypeStringTypeStringTypeDoubleTypeStringTypeStringTypeStringType
DateTypeDateTypeStringTypeStringTypeStringTypeStringTypeDateTypeTimestampTypeStringType
TimestampTypeTimestampTypeStringTypeStringTypeStringTypeStringTypeTimestampTypeTimestampTypeStringType
StringTypeStringTypeStringTypeStringTypeStringTypeStringTypeStringTypeStringTypeStringType

请注意,对于 DecimalType(38,0)*,上表有意不涵盖比例和精度的所有其他组合,因为目前我们只推断像 BigInteger/BigInt 这样的十进制类型。例如,1.1 被推断为 double 类型。

自 Spark 2.3 起,当广播哈希连接或广播嵌套循环连接适用时,我们优先广播在广播提示中明确指定的表。有关详细信息,请参阅 SQL 查询的连接策略提示 和 SPARK-22489。

自 Spark 2.3 起,当所有输入都是二进制时,functions.concat() 返回二进制输出。否则,它返回字符串。在 Spark 2.3 之前,它始终返回字符串,而不考虑输入类型。要保持旧行为,将 spark.sql.function.concatBinaryAsString 设置为 true。

自 Spark 2.3 起,当所有输入都是二进制时,SQL elt() 返回二进制输出。否则,它返回字符串。在 Spark 2.3 之前,它始终返回字符串,而不考虑输入类型。要保持旧行为,将 spark.sql.function.eltOutputAsString 设置为 true。

自 Spark 2.3 起,默认情况下,如果无法精确表示,十进制之间的算术运算返回一个四舍五入的值(而不是返回 NULL)。这符合 SQL ANSI 2011 规范和 Hive 2.2 中引入的 Hive 新行为(HIVE-15331)。这涉及以下更改:

  • 确定算术运算结果类型的规则已更新。特别是,如果所需的精度/比例超出可用值的范围,则比例最多减少 6 位,以防止截断十进制数的整数部分。所有算术运算都受此更改影响,即加法 (+)、减法 (-)、乘法 (*)、除法 (/)、余数 (%) 和正模数 (pmod)。
  • SQL 操作中使用的字面量值被转换为具有它们所需精确精度和比例的 DECIMAL
  • 引入了配置 spark.sql.decimalOperations.allowPrecisionLoss。它默认为 true,这意味着此处描述的新行为;如果设置为 false,Spark 使用先前规则,即它不调整表示值所需的比例,并且如果无法精确表示值,则返回 NULL

未别名的子查询的语义尚未明确定义,行为令人困惑。自 Spark 2.3 起,我们使此类令人困惑的情况无效,例如:SELECT v.i from (SELECT i FROM v),在这种情况下 Spark 将抛出分析异常,因为用户不应能够在子查询内使用限定符。有关更多详细信息,请参阅 SPARK-20690 和 SPARK-21335。

当使用 SparkSession.builder.getOrCreate() 创建 SparkSession 时,如果存在现有的 SparkContext,构建器会尝试使用指定给构建器的配置更新现有 SparkContextSparkConf,但 SparkContext 由所有 SparkSession 共享,因此我们不应更新它们。自 2.3 起,构建器不再更新配置。如果您想更新它们,需要在创建 SparkSession 之前更新它们。

从 Spark SQL 2.1 升级到 2.2

Spark 2.1.1 引入了一个新的配置键:spark.sql.hive.caseSensitiveInferenceMode。它有一个默认设置 NEVER_INFER,这保持了与 2.1.0 相同的行为。然而,Spark 2.2.0 将此设置的默认值更改为 INFER_AND_SAVE,以恢复与读取底层文件模式具有混合大小写列名的 Hive 元存储表的兼容性。使用 INFER_AND_SAVE 配置值,在首次访问时,Spark 将对任何尚未保存推断模式的 Hive 元存储表执行模式推断。请注意,对于具有数千个分区的表,模式推断可能非常耗时。如果与混合大小写列名的兼容性不是问题,您可以安全地将 spark.sql.hive.caseSensitiveInferenceMode 设置为 NEVER_INFER 以避免模式推断的初始开销。请注意,使用新的默认 INFER_AND_SAVE 设置,模式推断的结果将保存为元存储键以供将来使用。因此,初始模式推断仅在表的首次访问时发生。

自 Spark 2.2.1 和 2.3.0 起,当数据源表具有同时存在于分区模式和数据模式中的列时,模式总是在运行时推断。推断的模式不包含分区列。读取表时,Spark 尊重这些重叠列的分区值,而不是数据源文件中存储的值。在 2.2.0 和 2.1.x 版本中,推断的模式是分区的,但表的数据对用户不可见(即,结果集为空)。

自 Spark 2.2 起,视图定义以与先前版本不同的方式存储。这可能导致 Spark 无法读取先前版本创建的视图。在这种情况下,您需要使用较新的 Spark 版本通过 ALTER VIEW ASCREATE OR REPLACE VIEW AS 重新创建视图。

从 Spark SQL 2.0 升级到 2.1

数据源表现在将分区元数据存储在 Hive 元存储中。这意味着诸如 ALTER TABLE PARTITION ... SET LOCATION 之类的 Hive DDL 现在可用于使用数据源 API 创建的表。

旧数据源表可以通过 MSCK REPAIR TABLE 命令迁移到此格式。建议迁移旧表以利用 Hive DDL 支持和改进的规划性能。

要确定表是否已迁移,请在表上发出 DESCRIBE FORMATTED 时查找 PartitionProvider: Catalog 属性。

数据源表的 INSERT OVERWRITE TABLE ... PARTITION ... 行为更改。

在先前 Spark 版本中,INSERT OVERWRITE 覆盖整个数据源表,即使给定了分区规范。现在只有匹配规范的分区被覆盖。

请注意,这仍然与 Hive 表的行为不同,Hive 表的行为是仅覆盖与新插入数据重叠的分区。

从 Spark SQL 1.6 升级到 2.0

SparkSession 现在是 Spark 的新入口点,取代了旧的 SQLContextHiveContext。请注意,旧的 SQLContextHiveContext 为向后兼容而保留。可以从 SparkSession 访问新的目录接口 - 现有的数据库和表访问 API,如 listTablescreateExternalTabledropTempViewcacheTable 已移至此。

Dataset API 和 DataFrame API 已统一。在 Scala 中,DataFrame 成为 Dataset[Row] 的类型别名,而 Java API 用户必须将 DataFrame 替换为 Dataset<Row>。类型化转换(例如 mapfiltergroupByKey)和非类型化转换(例如 selectgroupBy)在 Dataset 类上都可用。由于 Python 和 R 中的编译时类型安全不是语言特性,Dataset 的概念不适用于这些语言的 API。相反,DataFrame 仍然是主要的编程抽象,这类似于这些语言中的单节点数据框概念。

Dataset 和 DataFrame API unionAll 已被弃用,并由 union 替换。

Dataset 和 DataFrame API explode 已被弃用,作为替代,使用 functions.explode()selectflatMap

Dataset 和 DataFrame API registerTempTable 已被弃用,并由 createOrReplaceTempView 替换。

Hive 表的 CREATE TABLE ... LOCATION 行为更改。

从 Spark 2.0 起,CREATE TABLE ... LOCATION 等同于 CREATE EXTERNAL TABLE ... LOCATION,以防止意外删除用户提供位置中的现有数据。这意味着,在 Spark SQL 中使用用户指定位置创建的 Hive 表始终是 Hive 外部表。删除外部表不会移除数据。不允许用户为 Hive 托管表指定位置。请注意,这与 Hive 行为不同。

因此,对这些表的 DROP TABLE 语句不会移除数据。

spark.sql.parquet.cacheMetadata 不再使用。有关详细信息,请参阅 SPARK-13664。

从 Spark SQL 1.5 升级到 1.6

从 Spark 1.6 起,默认情况下,Thrift 服务器在多会话模式下运行。这意味着每个 JDBC/ODBC 连接拥有其自己的 SQL 配置和临时函数注册表的副本。不过,缓存表仍然是共享的。如果您希望 Thrift 服务器在旧的单会话模式下运行,请将选项 spark.sql.hive.thriftServer.singleSession 设置为 true。您可以将此选项添加到 spark-defaults.conf,或通过 --conf 传递给 start-thriftserver.sh

./sbin/start-thriftserver.sh \--conf spark.sql.hive.thriftServer.singleSession=true \...

从 Spark 1.6 起,LongType 转换为 TimestampType 预期秒而不是微秒。此更改是为了匹配 Hive 1.2 的行为,以便从数字类型更一致地强制转换为 TimestampType。有关详细信息,请参阅 SPARK-11724。

从 Spark SQL 1.4 升级到 1.5

使用手动管理内存(Tungsten)的优化执行现在默认启用,同时启用了表达式求值的代码生成。这两个功能都可以通过将 spark.sql.tungsten.enabled 设置为 false 来禁用。

Parquet 模式合并不再默认启用。可以通过将 spark.sql.parquet.mergeSchema 设置为 true 重新启用。

内存列式存储分区修剪默认开启。可以通过将 spark.sql.inMemoryColumnarStorage.partitionPruning 设置为 false 来禁用。

不再支持无限精度十进制列,而是 Spark SQL 强制最大精度为 38。从 BigDecimal 对象推断模式时,现在使用精度 (38, 18)。当 DDL 中未指定精度时,默认值仍为 Decimal(10, 0)

时间戳现在以 1 微秒的精度存储,而不是 1 纳秒。

在 SQL 方言中,浮点数现在被解析为十进制。HiveQL 解析保持不变。

SQL/DataFrame 函数的规范名称现在是小写(例如,sumSUM)。

JSON 数据源不会自动加载由其他应用程序创建的新文件(即未通过 Spark SQL 插入到数据集中的文件)。对于 JSON 持久表(即表的元数据存储在 Hive Metastore 中),用户可以使用 REFRESH TABLE SQL 命令或 HiveContextrefreshTable 方法将这些新文件包含到表中。对于表示 JSON 数据集的 DataFrame,用户需要重新创建 DataFrame,新的 DataFrame 将包含新文件。

从 Spark SQL 1.3 升级到 1.4

DataFrame 数据读取器/写入器接口

根据用户反馈,我们创建了一个新的、更流畅的 API 用于读取数据(SQLContext.read)和写入数据(DataFrame.write),并弃用了旧的 API(例如 SQLContext.parquetFileSQLContext.jsonFile)。

有关更多信息,请参阅 SQLContext.read(Scala、Java、Python)和 DataFrame.write(Scala、Java、Python)的 API 文档。

DataFrame.groupBy 保留分组列
根据用户反馈,我们更改了 DataFrame.groupBy().agg() 的默认行为,以在结果 DataFrame 中保留分组列。要保持 1.3 中的行为,将 spark.sql.retainGroupColumns 设置为 false。

Python

import pyspark.sql.functions as func# 在 1.3.x 中,为了使分组列 "department" 显示出来,
# 必须将其作为 agg 函数调用的一部分显式包含。
df.groupBy("department").agg(df["department"], func.max("age"), func.sum("expense"))# 在 1.4+ 中,分组列 "department" 自动包含。
df.groupBy("department").agg(func.max("age"), func.sum("expense"))# 通过以下方式恢复到 1.3.x 行为(不保留分组列):
sqlContext.setConf("spark.sql.retainGroupColumns", "false")

DataFrame.withColumn 上的行为更改
在 1.4 之前,DataFrame.withColumn() 仅支持添加列。即使可能存在任何同名的现有列,该列也将始终作为具有指定名称的新列添加到结果 DataFrame 中。自 1.4 起,DataFrame.withColumn() 支持添加与所有现有列名称不同的列,或替换同名的现有列。

请注意,此更改仅适用于 Scala API,不适用于 PySpark 和 SparkR。

从 Spark SQL 1.0-1.2 升级到 1.3

在 Spark 1.3 中,我们移除了 Spark SQL 的“Alpha”标签,并在此过程中清理了可用的 API。从 Spark 1.3 开始,Spark SQL 将提供与 1.X 系列中其他版本的二进制兼容性。此兼容性保证不包括明确标记为不稳定(即 DeveloperAPIExperimental)的 API。

SchemaRDD 重命名为 DataFrame

升级到 Spark SQL 1.3 时用户将注意到的最大变化是 SchemaRDD 已重命名为 DataFrame。这主要是因为 DataFrame 不再直接继承自 RDD,而是通过自己的实现提供了 RDD 提供的大部分功能。DataFrame 仍然可以通过调用 .rdd 方法转换为 RDD。

在 Scala 中,有一个从 SchemaRDDDataFrame 的类型别名,以为某些用例提供源代码兼容性。仍然建议用户更新其代码以使用 DataFrame。Java 和 Python 用户需要更新其代码。

Java 和 Scala API 的统一

在 Spark 1.3 之前,有单独的 Java 兼容类(JavaSQLContextJavaSchemaRDD)镜像了 Scala API。在 Spark 1.3 中,Java API 和 Scala API 已统一。任何一种语言的用户都应使用 SQLContextDataFrame。通常,这些类尝试使用两种语言都可用的类型(例如 Array 而不是语言特定的集合)。在某些不存在公共类型的情况下(例如,用于传递闭包或 Map),改用函数重载。

此外,Java 特定类型 API 已被移除。Scala 和 Java 用户都应使用 org.apache.spark.sql.types 中的类以编程方式描述模式。

隐式转换的隔离和 dsl 包的移除(仅 Scala)

Spark 1.3 之前的许多代码示例以 import sqlContext._ 开头,这将 sqlContext 的所有函数引入作用域。在 Spark 1.3 中,我们将用于将 RDD 转换为 DataFrame 的隐式转换隔离到 SQLContext 内部的一个对象中。用户现在应编写 import sqlContext.implicits._

此外,隐式转换现在仅使用 toDF 方法增强由 Product(即案例类或元组)组成的 RDD,而不是自动应用。

在 DSL(现已被 DataFrame API 取代)内使用函数时,用户过去常常导入 org.apache.spark.sql.catalyst.dsl。相反,应使用公共 dataframe 函数 API:import org.apache.spark.sql.functions._

移除 org.apache.spark.sql 中 DataType 的类型别名(仅 Scala)

Spark 1.3 移除了基础 sql 包中存在的 DataType 的类型别名。用户应改为导入 org.apache.spark.sql.types 中的类。

UDF 注册移至 sqlContext.udf(Java 和 Scala)

用于注册 UDF 的函数,无论是用于 DataFrame DSL 还是 SQL,都已移至 SQLContext 中的 udf 对象。

sqlContext.udf.register("strLen", (s: String) => s.length())
sqlContext.udf().register("strLen", (String s) -> s.length(), DataTypes.IntegerType);

Python UDF 注册不变。

与 Apache Hive 的兼容性

Spark SQL 旨在与 Hive Metastore、SerDes 和 UDF 兼容。目前,Hive SerDes 和 UDF 基于内置 Hive,并且 Spark SQL 可以连接到不同版本的 Hive Metastore(从 0.12.0 到 2.3.9 和 3.0.0 到 3.1.3。另请参阅与不同版本的 Hive Metastore 交互)。

在现有 Hive 仓库中部署

Spark SQL Thrift JDBC 服务器旨在“开箱即用”与现有 Hive 安装兼容。您不需要修改现有的 Hive Metastore 或更改表的数据放置或分区。

支持的 Hive 功能

Spark SQL 支持绝大多数 Hive 功能,例如:

  • Hive 查询语句,包括:
    • SELECT
    • GROUP BY
    • ORDER BY
    • DISTRIBUTE BY
    • CLUSTER BY
    • SORT BY
  • 所有 Hive 运算符,包括:
    • 关系运算符(=, <=>, ==, <>, <, >, >=, <= 等)
    • 算术运算符(+, -, *, /, % 等)
    • 逻辑运算符(AND, OR 等)
    • 复杂类型构造函数
    • 数学函数(sign, ln, cos 等)
    • 字符串函数(instr, length, printf 等)
    • 用户定义函数(UDF)
    • 用户定义聚合函数(UDAF)
    • 用户定义序列化格式(SerDes)
    • 窗口函数
  • 连接
    • JOIN
    • {LEFT|RIGHT|FULL} OUTER JOIN
    • LEFT SEMI JOIN
    • LEFT ANTI JOIN
    • CROSS JOIN
  • 联合
  • 子查询
    • FROM 子句中的子查询
      SELECT col FROM (SELECT a + b AS col FROM t1) t2
      
    • WHERE 子句中的子查询
      • WHERE 子句中的相关或非相关 INNOT IN 语句
        SELECT col FROM t1 WHERE col IN (SELECT a FROM t2 WHERE t1.a = t2.a)
        SELECT col FROM t1 WHERE col IN (SELECT a FROM t2)
        
      • WHERE 子句中的相关或非相关 EXISTSNOT EXISTS 语句
        SELECT col FROM t1 WHERE EXISTS (SELECT t2.a FROM t2 WHERE t1.a = t2.a AND t2.a > 10)
        SELECT col FROM t1 WHERE EXISTS (SELECT t2.a FROM t2 WHERE t2.a > 10)
        
    • JOIN 条件中的非相关 INNOT IN 语句
      SELECT t1.col FROM t1 JOIN t2 ON t1.a = t2.a AND t1.a IN (SELECT a FROM t3)
      
    • JOIN 条件中的非相关 EXISTSNOT EXISTS 语句
      SELECT t1.col FROM t1 JOIN t2 ON t1.a = t2.a AND EXISTS (SELECT * FROM t3 WHERE t3.a > 10)
      
  • 抽样
  • 解释
  • 分区表,包括动态分区插入
  • 视图
    • 如果视图定义查询中未指定列别名,Spark 和 Hive 都会生成别名名称,但方式不同。为了使 Spark 能够读取 Hive 创建的视图,用户应在视图定义查询中显式指定列别名。例如,Spark 无法读取由 Hive 创建的如下 v1
      CREATE VIEW v1 AS SELECT * FROM (SELECT c + 1 FROM (SELECT 1 c) t1) t2;
      
      相反,您应如下显式指定列别名创建 v1
      CREATE VIEW v1 AS SELECT * FROM (SELECT c + 1 AS inc_c FROM (SELECT 1 c) t1) t2;
      
  • 所有 Hive DDL 函数,包括:
    • CREATE TABLE
    • CREATE TABLE AS SELECT
    • CREATE TABLE LIKE
    • ALTER TABLE
  • 大多数 Hive 数据类型,包括:
    • TINYINT
    • SMALLINT
    • INT
    • BIGINT
    • BOOLEAN
    • FLOAT
    • DOUBLE
    • STRING
    • BINARY
    • TIMESTAMP
    • DATE
    • ARRAY<>
    • MAP<>
    • STRUCT<>

不支持的 Hive 功能

以下是我们尚不支持的 Hive 功能列表。这些功能大多数在 Hive 部署中很少使用。

  • 深奥的 Hive 功能
    • UNION 类型
    • 唯一连接
    • 列统计信息收集:Spark SQL 目前不附带扫描来收集列统计信息,仅支持填充 hive 元存储的 sizeInBytes 字段。
  • Hive 输入/输出格式
    • CLI 的文件格式:对于显示回 CLI 的结果,Spark SQL 仅支持 TextOutputFormat
    • Hadoop 归档
  • Hive 优化
    • 少数 Hive 优化尚未包含在 Spark 中。其中一些(例如索引)由于 Spark SQL 的内存计算模型而不太重要。其他计划在 Spark SQL 的未来版本中提供。
    • 块级位图索引和虚拟列(用于构建索引)
    • 自动确定连接和分组依据的 reducer 数量:目前,在 Spark SQL 中,您需要使用 SET spark.sql.shuffle.partitions=[num_tasks]; 来控制洗牌后的并行度。
    • 仅元数据查询:对于可以仅使用元数据回答的查询,Spark SQL 仍然启动任务来计算结果。
    • 倾斜数据标志:Spark SQL 不遵循 Hive 中的倾斜数据标志。
    • 连接中的 STREAMTABLE 提示:Spark SQL 不遵循 STREAMTABLE 提示。
    • 合并查询结果的多个小文件:如果结果输出包含多个小文件,Hive 可以选择将小文件合并为更少的大文件以避免 HDFS 元数据溢出。Spark SQL 不支持此功能。
  • Hive UDF/UDTF/UDAF
    • 并非所有 Hive UDF/UDTF/UDAF 的 API 都受 Spark SQL 支持。以下是不支持的 API:
      • getRequiredJarsgetRequiredFilesUDFGenericUDF)是自动包含此 UDF 所需的附加资源的函数。
      • GenericUDTF 中的 initialize(StructObjectInspector) 尚不支持。Spark SQL 目前仅使用已弃用的接口 initialize(ObjectInspector[])
      • configureGenericUDFGenericUDTFGenericUDAFEvaluator)是一个使用 MapredContext 初始化函数的函数,这不适用于 Spark。
      • closeGenericUDFGenericUDAFEvaluator)是一个释放相关资源的函数。Spark SQL 在任务完成时不调用此函数。
      • resetGenericUDAFEvaluator)是一个重新初始化聚合以重用相同聚合的函数。Spark SQL 目前不支持聚合的重用。
      • getWindowingEvaluatorGenericUDAFEvaluator)是一个通过评估固定窗口上的聚合来优化聚合的函数。
  • 不兼容的 Hive UDF
    • 以下是 Hive 和 Spark 生成不同结果的场景:
      • SQRT(n):如果 n < 0,Hive 返回 null,Spark SQL 返回 NaN
      • ACOS(n):如果 n < -1 或 n > 1,Hive 返回 null,Spark SQL 返回 NaN
      • ASIN(n):如果 n < -1 或 n > 1,Hive 返回 null,Spark SQL 返回 NaN
      • CAST(n AS TIMESTAMP):如果 n 是整数,Hive 将 n 视为毫秒,Spark SQL 将 n 视为秒。

Spark SQL 参考指南

Spark SQL 是 Apache Spark 中用于处理结构化数据的模块。本指南是结构化查询语言(SQL)的参考手册,包含了常用 SQL 用法的语法、语义、关键词和示例。它包含以下主题的信息:

ANSI 合规性

在 Spark SQL 中,有两个选项用于符合 SQL 标准:spark.sql.ansi.enabledspark.sql.storeAssignmentPolicy(详情见下表)。

spark.sql.ansi.enabled 设置为 true 时,Spark SQL 使用符合 ANSI 标准的方言,而不是符合 Hive 的方言。例如,如果 SQL 运算符/函数的输入无效,Spark 将在运行时抛出异常,而不是返回 null 结果。一些 ANSI 方言特性可能并非直接来自 ANSI SQL 标准,但它们的行为与 ANSI SQL 的风格一致。

此外,Spark SQL 有一个独立的选项来控制向表中插入行时的隐式转换行为。这些转换行为在标准中被定义为存储分配规则。

spark.sql.storeAssignmentPolicy 设置为 ANSI 时,Spark SQL 遵循 ANSI 存储分配规则。这是一个独立的配置,因为它的默认值是 ANSI,而配置 spark.sql.ansi.enabled 默认是禁用的。

属性名称默认值含义自版本
spark.sql.ansi.enabledfalse当为 true 时,Spark 尝试符合 ANSI SQL 规范:
1. Spark SQL 将在无效操作时抛出运行时异常,包括整数溢出错误、字符串解析错误等。
2. Spark 将使用不同的类型强制转换规则来解决数据类型之间的冲突。这些规则一致地基于数据类型优先级。
3.0.0
spark.sql.storeAssignmentPolicyANSI当将值插入到具有不同数据类型的列时,Spark 将执行类型转换。目前,我们支持 3 种类型强制转换规则策略:ANSI、legacy 和 strict。
1. 使用 ANSI 策略时,Spark 按照 ANSI SQL 执行类型强制转换。实际上,其行为与 PostgreSQL 基本相同。它不允许某些不合理的类型转换,例如将字符串转换为整数或将双精度浮点数转换为布尔值。在插入数值类型列时,如果值超出目标数据类型的范围,将抛出溢出错误。
2. 使用 legacy 策略时,Spark 允许任何有效的 Cast 进行类型强制转换,这非常宽松。例如,允许将字符串转换为整数或将双精度浮点数转换为布尔值。这也是 Spark 2.x 中的唯一行为,并且与 Hive 兼容。
3. 使用 strict 策略时,Spark 不允许类型强制转换中任何可能的精度损失或数据截断,例如,不允许将双精度浮点数转换为整数或将十进制数转换为双精度浮点数。
3.0.0

以下小节介绍了启用 ANSI 模式时,算术运算、类型转换和 SQL 解析方面的行为变化。对于 Spark SQL 中的类型转换,有三种类型,本文将逐一介绍:cast、存储分配和类型强制转换。

算术运算
在 Spark SQL 中,默认情况下,对数值类型(十进制数除外)执行的算术运算不检查溢出。这意味着如果操作导致溢出,结果与 Java/Scala 程序中相应操作的结果相同(例如,如果两个整数的和高于可表示的最大值,则结果为负数)。另一方面,Spark SQL 对于十进制溢出返回 null。当 spark.sql.ansi.enabled 设置为 true 并且在数值和间隔算术运算中发生溢出时,它会在运行时抛出算术异常。

-- `spark.sql.ansi.enabled=true`
SELECT 2147483647 + 1;
-- org.apache.spark.SparkArithmeticException: [ARITHMETIC_OVERFLOW] integer overflow. Use 'try_add' to tolerate overflow and return NULL instead. If necessary set spark.sql.ansi.enabled to "false" to bypass this error.
-- == SQL(line 1, position 8) ==
-- SELECT 2147483647 + 1
--        ^^^^^^^^^^^^^^SELECT abs(-2147483648);
-- org.apache.spark.SparkArithmeticException: [ARITHMETIC_OVERFLOW] integer overflow. If necessary set spark.sql.ansi.enabled to "false" to bypass this error.-- `spark.sql.ansi.enabled=false`
SELECT 2147483647 + 1;
-- +----------------+
-- |(2147483647 + 1)|
-- +----------------+
-- |     -2147483648|
-- +----------------+SELECT abs(-2147483648);
-- +----------------+
-- |abs(-2147483648)|
-- +----------------+
-- |     -2147483648|
-- +----------------+

Cast
spark.sql.ansi.enabled 设置为 true 时,通过 CAST 语法进行的显式转换会为标准中定义的非法转换模式抛出运行时异常,例如从字符串到整数的转换。

此外,ANSI SQL 模式禁止以下在 ANSI 模式关闭时允许的类型转换:

  • 数值类型 <=> 二进制类型
  • 日期类型 <=> 布尔类型
  • 时间戳类型 <=> 布尔类型
  • 日期类型 => 数值类型

CAST 表达式中源数据类型和目标数据类型的有效组合由下表给出。“Y” 表示该组合在语法上有效且无限制,“N” 表示该组合无效。

源\目标数值字符串日期时间戳Timestamp_NTZ间隔布尔二进制数组映射结构体
数值YYNYNYYNNNN
字符串YYYYYYYYNNN
日期NYYYYNNNNNN
时间戳YYYYYNNNNNN
Timestamp_NTZNYYYYNNNNNN
间隔YYNNNYNNNNN
布尔YYNNNNYNNNN
二进制NYNNNNNYNNN
数组NYNNNNNNYNN
映射NYNNNNNNNYN
结构体NYNNNNNNNNY

在上表中,所有使用新语法的 CAST 被标记为红色的 Y:

  • CAST(数值 AS 数值):如果值超出目标数据类型的范围,则抛出溢出异常。
  • CAST(字符串 AS (数值/日期/时间戳/Timestamp_NTZ/间隔/布尔)):如果值无法解析为目标数据类型,则抛出运行时异常。
  • CAST(时间戳 AS 数值):如果自纪元起的秒数超出目标数据类型的范围,则抛出溢出异常。
  • CAST(数值 AS 时间戳):如果数值乘以 1000000(每秒微秒数)超出 Long 类型的范围,则抛出溢出异常。
  • CAST(数组 AS 数组):如果在元素转换过程中出现任何问题,则抛出异常。
  • CAST(映射 AS 映射):如果在键和值的转换过程中出现任何问题,则抛出异常。
  • CAST(结构体 AS 结构体):如果在结构体字段的转换过程中出现任何问题,则抛出异常。
  • CAST(数值 AS 字符串):在将十进制值转换为字符串时,始终使用纯字符串表示形式,而不是在需要指数时使用科学计数法。
  • CAST(间隔 AS 数值):如果日时间间隔的微秒数或年月间隔的月数超出目标数据类型的范围,则抛出溢出异常。
  • CAST(数值 AS 间隔):如果数值乘以目标间隔的结束单位超出年月间隔的 Int 类型范围或日时间间隔的 Long 类型范围,则抛出溢出异常。
-- 显式转换示例-- `spark.sql.ansi.enabled=true`
SELECT CAST('a' AS INT);
-- org.apache.spark.SparkNumberFormatException: [CAST_INVALID_INPUT] The value 'a' of the type "STRING" cannot be cast to "INT" because it is malformed. Correct the value as per the syntax, or change its target type. Use `try_cast` to tolerate malformed input and return NULL instead. If necessary set "spark.sql.ansi.enabled" to "false" to bypass this error.
-- == SQL(line 1, position 8) ==
-- SELECT CAST('a' AS INT)
--        ^^^^^^^^^^^^^^^^SELECT CAST(2147483648L AS INT);
-- org.apache.spark.SparkArithmeticException: [CAST_OVERFLOW] The value 2147483648L of the type "BIGINT" cannot be cast to "INT" due to an overflow. Use `try_cast` to tolerate overflow and return NULL instead. If necessary set "spark.sql.ansi.enabled" to "false" to bypass this error.SELECT CAST(DATE'2020-01-01' AS INT);
-- org.apache.spark.sql.AnalysisException: cannot resolve 'CAST(DATE '2020-01-01' AS INT)' due to data type mismatch: cannot cast date to int.
-- To convert values from date to int, you can use function UNIX_DATE instead.-- `spark.sql.ansi.enabled=false` (这是默认行为)
SELECT CAST('a' AS INT);
-- +--------------+
-- |CAST(a AS INT)|
-- +--------------+
-- |          null|
-- +--------------+SELECT CAST(2147483648L AS INT);
-- +-----------------------+
-- |CAST(2147483648 AS INT)|
-- +-----------------------+
-- |            -2147483648|
-- +-----------------------+SELECT CAST(DATE'2020-01-01' AS INT)
-- +------------------------------+
-- |CAST(DATE '2020-01-01' AS INT)|
-- +------------------------------+
-- |                          null|
-- +------------------------------+-- 存储分配规则示例
CREATE TABLE t (v INT);-- `spark.sql.storeAssignmentPolicy=ANSI`
INSERT INTO t VALUES ('1');
-- org.apache.spark.sql.AnalysisException: [INCOMPATIBLE_DATA_FOR_TABLE.CANNOT_SAFELY_CAST] Cannot write incompatible data for table `spark_catalog`.`default`.`t`: Cannot safely cast `v`: "STRING" to "INT".-- `spark.sql.storeAssignmentPolicy=LEGACY` (这是 Spark 2.x 及之前的遗留行为)
INSERT INTO t VALUES ('1');
SELECT * FROM t;
-- +---+
-- |  v|
-- +---+
-- |  1|
-- +---+

Cast 中的舍入
当将带有小数的十进制数转换为以 SECOND 作为结束单位的间隔类型(如 INTERVAL HOUR TO SECOND)时,Spark 将小数部分向"最近邻"舍入,除非两个邻居等距,在这种情况下向上舍入。

存储分配
如开头所述,当 spark.sql.storeAssignmentPolicy 设置为 ANSI(这是默认值)时,Spark SQL 在表插入时遵循 ANSI 存储分配规则。表插入中源数据类型和目标数据类型的有效组合由下表给出。

源\目标数值字符串日期时间戳Timestamp_NTZ间隔布尔二进制数组映射结构体
数值YYNNNNNNNNN
字符串NYNNNNNNNNN
日期NYYYYNNNNNN
时间戳NYYYYNNNNNN
Timestamp_NTZNYYYYNNNNNN
间隔NYNNNN*NNNNN
布尔NYNNNNYNNNN
二进制NYNNNNNYNNN
数组NNNNNNNNY**NN
映射NNNNNNNNNY**N
结构体NNNNNNNNNNY**
  • Spark 不支持间隔类型的表列。
    ** 对于数组/映射/结构体类型,数据类型检查规则递归地应用于其组件元素。

在表插入期间,Spark 将在数值溢出时抛出异常。

CREATE TABLE test(i INT);INSERT INTO test VALUES (2147483648L);
-- org.apache.spark.SparkArithmeticException: [CAST_OVERFLOW_IN_TABLE_INSERT] Fail to insert a value of "BIGINT" type into the "INT" type column `i` due to an overflow. Use `try_cast` on the input value to tolerate overflow and return NULL instead.

类型强制转换
类型提升和优先级
spark.sql.ansi.enabled 设置为 true 时,Spark SQL 使用几个规则来管理如何解决数据类型之间的冲突。此冲突解决的核心是类型优先级列表,它定义了给定数据类型的值是否可以隐式提升为另一种数据类型。

数据类型优先级列表(从最窄到最宽)
ByteByte -> Short -> Int -> Long -> Decimal -> Float* -> Double
ShortShort -> Int -> Long -> Decimal -> Float* -> Double
IntInt -> Long -> Decimal -> Float* -> Double
LongLong -> Decimal -> Float* -> Double
DecimalDecimal -> Float* -> Double
FloatFloat -> Double
DoubleDouble
DateDate -> Timestamp_NTZ -> Timestamp
TimestampTimestamp
StringString, Long -> Double, Date -> Timestamp_NTZ -> Timestamp , Boolean, Binary **
BinaryBinary
BooleanBoolean
IntervalInterval
MapMap***
ArrayArray***
StructStruct***
  • 对于最小公共类型解析,跳过 float 以避免精度损失。
    ** 字符串可以提升为多种数据类型。注意 Byte/Short/Int/Decimal/Float 不在此优先级列表中。Byte/Short/Int 和 String 的最小公共类型是 Long,而 Decimal/Float 和 String 的最小公共类型是 Double。
    *** 对于复杂类型,优先级规则递归地应用于其组件元素。

特殊规则适用于无类型的 NULL。NULL 可以提升为任何其他类型。

这是一个将优先级列表描绘为有向树的图形表示:类型优先级列表

图片

最小公共类型解析
一组类型的最小公共类型是从该类型集合的所有元素都可以通过优先级列表到达的最窄类型。

最小公共类型解析用于:

  • 为期望多个参数具有共享参数类型的函数(例如 coalesceleastgreatest)推导参数类型。
  • 为运算符(例如算术运算或比较)推导操作数类型。
  • 为表达式(例如 case 表达式)推导结果类型。
  • 为数组和映射构造函数推导元素、键或值类型。如果最小公共类型解析为 FLOAT,则应用特殊规则。对于 float 类型值,如果任何类型是 INT、BIGINT 或 DECIMAL,则将最小公共类型提升为 DOUBLE 以避免潜在的位数损失。
-- coalesce 函数接受任何一组参数类型,只要它们共享一个最小公共类型。
-- 结果类型是参数的最小公共类型。
> SET spark.sql.ansi.enabled=true;
> SELECT typeof(coalesce(1Y, 1L, NULL));
-- BIGINT
> SELECT typeof(coalesce(1, DATE'2020-01-01'));
-- Error: Incompatible types [INT, DATE]> SELECT typeof(coalesce(ARRAY(1Y), ARRAY(1L)));
-- ARRAY<BIGINT>
> SELECT typeof(coalesce(1, 1F));
-- DOUBLE
> SELECT typeof(coalesce(1L, 1F));
-- DOUBLE
> SELECT (typeof(coalesce(1BD, 1F)));
-- DOUBLE> SELECT typeof(coalesce(1, '2147483648'))
-- BIGINT
> SELECT typeof(coalesce(1.0, '2147483648'))
-- DOUBLE
> SELECT typeof(coalesce(DATE'2021-01-01', '2022-01-01'))
-- DATE

SQL 函数
函数调用
在 ANSI 模式下(spark.sql.ansi.enabled=true),Spark SQL 的函数调用:

  • 通常,它遵循存储分配规则,将输入值存储为 SQL 函数声明的参数类型。
  • 特殊规则适用于无类型的 NULL。NULL 可以提升为任何其他类型。
> SET spark.sql.ansi.enabled=true;
-- 隐式将 Int 转换为 String 类型
> SELECT concat('total number: ', 1);
-- total number: 1
-- 隐式将 Timestamp 转换为 Date 类型
> select datediff(now(), current_date);
-- 0-- 隐式将 String 转换为 Double 类型
> SELECT ceil('0.1');
-- 1
-- 特殊规则:隐式将 NULL 转换为 Date 类型
> SELECT year(null);
-- NULL> CREATE TABLE t(s string);
-- 不能将 String 列存储为数值类型。
> SELECT ceil(s) from t;
-- Error in query: cannot resolve 'CEIL(spark_catalog.default.t.s)' due to data type mismatch
-- 不能将 String 列存储为 Date 类型。
> select year(s) from t;
-- Error in query: cannot resolve 'year(spark_catalog.default.t.s)' due to data type mismatch

具有不同行为的函数
某些 SQL 函数的行为在 ANSI 模式下(spark.sql.ansi.enabled=true)可能不同。

  • size:此函数对 null 输入返回 null。
  • element_at:如果使用无效索引,此函数抛出 ArrayIndexOutOfBoundsException
  • elt:如果使用无效索引,此函数抛出 ArrayIndexOutOfBoundsException
  • parse_url:如果输入字符串不是有效的 url,此函数抛出 IllegalArgumentException
  • to_date:如果输入字符串无法解析或模式字符串无效,此函数应失败并抛出异常。
  • to_timestamp:如果输入字符串无法解析或模式字符串无效,此函数应失败并抛出异常。
  • unix_timestamp:如果输入字符串无法解析或模式字符串无效,此函数应失败并抛出异常。
  • to_unix_timestamp:如果输入字符串无法解析或模式字符串无效,此函数应失败并抛出异常。
  • make_date:如果结果日期无效,此函数应失败并抛出异常。
  • make_timestamp:如果结果时间戳无效,此函数应失败并抛出异常。
  • make_interval:如果结果间隔无效,此函数应失败并抛出异常。
  • next_day:如果输入不是有效的星期几,此函数抛出 IllegalArgumentException

SQL 运算符
某些 SQL 运算符的行为在 ANSI 模式下(spark.sql.ansi.enabled=true)可能不同。

  • array_col[index]:如果使用无效索引,此运算符抛出 ArrayIndexOutOfBoundsException

ANSI 模式下的有用函数
当 ANSI 模式开启时,它会对无效操作抛出异常。您可以使用以下 SQL 函数来抑制此类异常。

  • try_cast:与 CAST 相同,不同之处在于它在运行时错误时返回 NULL 结果而不是抛出异常。
  • try_add:与加法运算符 + 相同,不同之处在于它在整数值溢出时返回 NULL 结果而不是抛出异常。
  • try_subtract:与减法运算符 - 相同,不同之处在于它在整数值溢出时返回 NULL 结果而不是抛出异常。
  • try_multiply:与乘法运算符 * 相同,不同之处在于它在整数值溢出时返回 NULL 结果而不是抛出异常。
  • try_divide:与除法运算符 / 相同,不同之处在于它在除以 0 时返回 NULL 结果而不是抛出异常。
  • try_sum:与函数 sum 相同,不同之处在于它在整数/十进制/间隔值溢出时返回 NULL 结果而不是抛出异常。
  • try_avg:与函数 avg 相同,不同之处在于它在十进制/间隔值溢出时返回 NULL 结果而不是抛出异常。
  • try_element_at:与函数 element_at 相同,不同之处在于它在数组索引越界时返回 NULL 结果而不是抛出异常。
  • try_to_timestamp:与函数 to_timestamp 相同,不同之处在于它在字符串解析错误时返回 NULL 结果而不是抛出异常。

SQL 关键字(可选,默认禁用)
spark.sql.ansi.enabledspark.sql.ansi.enforceReservedKeywords 都为 true 时,Spark SQL 将使用 ANSI 模式解析器。

使用 ANSI 模式解析器时,Spark SQL 有两种关键字:

  • 非保留关键字:仅在特定上下文中具有特殊含义,并且可以在其他上下文中用作标识符的关键字。例如,EXPLAIN SELECT ... 是一个命令,但 EXPLAIN 可以在其他地方用作标识符。
  • 保留关键字:被保留且不能用作表、视图、列、函数、别名等标识符的关键字。

使用默认解析器时,Spark SQL 有两种关键字:

  • 非保留关键字:与启用 ANSI 模式时的定义相同。
  • 严格非保留关键字:非保留关键字的严格版本,不能用作表别名。

默认情况下,spark.sql.ansi.enabledspark.sql.ansi.enforceReservedKeywords 都为 false。

以下是 Spark SQL 中所有关键字的列表。

关键字Spark SQL ANSI 模式Spark SQL 默认模式SQL-2016
ADDnon-reservednon-reservednon-reserved
AFTERnon-reservednon-reservednon-reserved
ALLreservednon-reservedreserved
ALTERnon-reservednon-reservedreserved
ZONEnon-reservednon-reservednon-reserved

(此处省略了中间大量的关键字列表,内容与英文原文相同,只是将状态描述翻译成了中文。完整的列表很长,但翻译模式一致:保留关键字 -> 保留,非保留关键字 -> 非保留,严格非保留关键字 -> 严格非保留,不是关键字 -> 不是关键字。)

数据类型

支持的数据类型
Spark SQL 和 DataFrames 支持以下数据类型:

数值类型

  • ByteType:表示 1 字节有符号整数。数值范围从 -128 到 127。
  • ShortType:表示 2 字节有符号整数。数值范围从 -32768 到 32767。
  • IntegerType:表示 4 字节有符号整数。数值范围从 -2147483648 到 2147483647。
  • LongType:表示 8 字节有符号整数。数值范围从 -9223372036854775808 到 9223372036854775807。
  • FloatType:表示 4 字节单精度浮点数。
  • DoubleType:表示 8 字节双精度浮点数。
  • DecimalType:表示任意精度的有符号十进制数。内部由 java.math.BigDecimal 支持。BigDecimal 由一个任意精度的整数未缩放值和一个 32 位整数比例组成。

字符串类型

  • StringType:表示字符串值。
  • VarcharType(length)StringType 的一种变体,具有长度限制。如果输入字符串超过长度限制,数据写入将失败。注意:此类型只能用于表模式,不能用于函数/运算符。
  • CharType(length):固定长度的 VarcharType(length) 变体。读取 CharType(n) 类型的列总是返回长度为 n 的字符串值。Char 类型列的比较会将较短的一个填充到较长的长度。

二进制类型

  • BinaryType:表示字节序列值。

布尔类型

  • BooleanType:表示布尔值。

日期时间类型

  • DateType:表示包含年、月、日字段的值,没有时区。
  • TimestampType:带本地时区的时间戳(TIMESTAMP_LTZ)。它表示包含年、月、日、时、分、秒字段的值,使用会话本地时区。时间戳值代表时间上的一个绝对点。
  • TimestampNTZType:不带时区的时间戳(TIMESTAMP_NTZ)。它表示包含年、月、日、时、分、秒字段的值。所有操作均不考虑任何时区。

注意:Spark 中的 TIMESTAMP 是与 TIMESTAMP_LTZTIMESTAMP_NTZ 变体之一相关联的用户指定别名。用户可以通过配置 spark.sql.timestampType 将默认时间戳类型设置为 TIMESTAMP_LTZ(默认值)或 TIMESTAMP_NTZ

间隔类型

  • YearMonthIntervalType(startField, endField):表示一个年月间隔,由以下字段的连续子集组成:

    • MONTH,年份内的月份 [0…11],
    • YEAR,年份范围 [0…178956970]。
      单个间隔字段是非负的,但间隔本身可以有符号,并且可以是负数。

    startField 是最左边的字段,endField 是类型的最右边字段。startFieldendField 的有效值是 0(MONTH)和 1(YEAR)。支持的年月间隔类型有:

    年月间隔类型SQL 类型类型实例
    YearMonthIntervalType(YEAR, YEAR)YearMonthIntervalType(YEAR)INTERVAL YEARINTERVAL '2021' YEAR
    YearMonthIntervalType(YEAR, MONTH)INTERVAL YEAR TO MONTHINTERVAL '2021-07' YEAR TO MONTH
    YearMonthIntervalType(MONTH, MONTH)YearMonthIntervalType(MONTH)INTERVAL MONTHINTERVAL '10' MONTH
  • DayTimeIntervalType(startField, endField):表示一个日时间间隔,由以下字段的连续子集组成:

    • SECOND,分钟内的秒数,可能还有秒的小数部分 [0…59.999999],
    • MINUTE,小时内分钟数 [0…59],
    • HOUR,天内小时数 [0…23],
    • DAY,天数范围 [0…106751991]。
      单个间隔字段是非负的,但间隔本身可以有符号,并且可以是负数。

    startField 是最左边的字段,endField 是类型的最右边字段。startFieldendField 的有效值是 0(DAY)、1(HOUR)、2(MINUTE)、3(SECOND)。支持的日时间间隔类型有:

    日时间间隔类型SQL 类型类型实例
    DayTimeIntervalType(DAY, DAY)DayTimeIntervalType(DAY)INTERVAL DAYINTERVAL '100' DAY
    DayTimeIntervalType(DAY, HOUR)INTERVAL DAY TO HOURINTERVAL '100 10' DAY TO HOUR
    DayTimeIntervalType(DAY, MINUTE)INTERVAL DAY TO MINUTEINTERVAL '100 10:30' DAY TO MINUTE
    DayTimeIntervalType(DAY, SECOND)INTERVAL DAY TO SECONDINTERVAL '100 10:30:40.999999' DAY TO SECOND
    DayTimeIntervalType(HOUR, HOUR)DayTimeIntervalType(HOUR)INTERVAL HOURINTERVAL '123' HOUR
    DayTimeIntervalType(HOUR, MINUTE)INTERVAL HOUR TO MINUTEINTERVAL '123:10' HOUR TO MINUTE
    DayTimeIntervalType(HOUR, SECOND)INTERVAL HOUR TO SECONDINTERVAL '123:10:59' HOUR TO SECOND
    DayTimeIntervalType(MINUTE, MINUTE)DayTimeIntervalType(MINUTE)INTERVAL MINUTEINTERVAL '1000' MINUTE
    DayTimeIntervalType(MINUTE, SECOND)INTERVAL MINUTE TO SECONDINTERVAL '1000:01.001' MINUTE TO SECOND
    DayTimeIntervalType(SECOND, SECOND)DayTimeIntervalType(SECOND)INTERVAL SECONDINTERVAL '1000.000001' SECOND

复杂类型

  • ArrayType(elementType, containsNull):表示包含一系列具有 elementType 类型的元素的值。containsNull 用于指示 ArrayType 值中的元素是否可以具有 null 值。
  • MapType(keyType, valueType, valueContainsNull):表示包含一组键值对的值。键的数据类型由 keyType 描述,值的数据类型由 valueType 描述。对于 MapType 值,键不允许为 null 值。valueContainsNull 用于指示 MapType 值中的值是否可以具有 null 值。
  • StructType(fields):表示具有由一系列 StructFieldfields)描述的结构的值。
  • StructField(name, dataType, nullable):表示 StructType 中的一个字段。字段的名称由 name 指示。字段的数据类型由 dataType 指示。nullable 用于指示这些字段的值是否可以具有 null 值。

Python

Spark SQL 的所有数据类型都位于 pyspark.sql.types 包中。您可以通过以下方式访问它们:

from pyspark.sql.types import *
数据类型Python 中的值类型访问或创建数据类型的 API
ByteTypeintlong
注意:数字将在运行时转换为 1 字节有符号整数。请确保数字在 -128 到 127 的范围内。
ByteType()
ShortTypeintlong
注意:数字将在运行时转换为 2 字节有符号整数。请确保数字在 -32768 到 32767 的范围内。
ShortType()
IntegerTypeintlongIntegerType()
LongTypelong
注意:数字将在运行时转换为 8 字节有符号整数。请确保数字在 -9223372036854775808 到 9223372036854775807 的范围内。否则,请将数据转换为 decimal.Decimal 并使用 DecimalType
LongType()
FloatTypefloat
注意:数字将在运行时转换为 4 字节单精度浮点数。
FloatType()
DoubleTypefloatDoubleType()
DecimalTypedecimal.DecimalDecimalType()
StringTypestringStringType()
BinaryTypebytearrayBinaryType()
BooleanTypeboolBooleanType()
TimestampTypedatetime.datetimeTimestampType()
TimestampNTZTypedatetime.datetimeTimestampNTZType()
DateTypedatetime.dateDateType()
DayTimeIntervalTypedatetime.timedeltaDayTimeIntervalType()
ArrayTypelist, tuplearrayArrayType(elementType, [containsNull])
注意:containsNull 的默认值为 True
MapTypedictMapType(keyType, valueType, [valueContainsNull])
注意:valueContainsNull 的默认值为 True
StructTypelisttupleStructType(fields)
注意:fieldsStructField 的一个序列。此外,不允许有两个同名字段。
StructField此字段数据类型的 Python 值类型
(例如,对于数据类型为 IntegerTypeStructField,值为 int
StructField(name, dataType, [nullable])
注意:nullable 的默认值为 True

Scala

Spark SQL 的所有数据类型都位于 org.apache.spark.sql.types 包中。您可以通过以下方式访问它们:

import org.apache.spark.sql.types._

在 Spark 仓库的 “examples/src/main/scala/org/apache/spark/examples/sql/SparkSQLExample.scala” 中找到完整的示例代码。

数据类型Scala 中的值类型访问或创建数据类型的 API
ByteTypeByteByteType
ShortTypeShortShortType
IntegerTypeIntIntegerType
LongTypeLongLongType
FloatTypeFloatFloatType
DoubleTypeDoubleDoubleType
DecimalTypejava.math.BigDecimalDecimalType
StringTypeStringStringType
BinaryTypeArray[Byte]BinaryType
BooleanTypeBooleanBooleanType
TimestampTypejava.time.Instantjava.sql.TimestampTimestampType
TimestampNTZTypejava.time.LocalDateTimeTimestampNTZType
DateTypejava.time.LocalDatejava.sql.DateDateType
YearMonthIntervalTypejava.time.PeriodYearMonthIntervalType
DayTimeIntervalTypejava.time.DurationDayTimeIntervalType
ArrayTypescala.collection.SeqArrayType(elementType, [containsNull])
注意:containsNull 的默认值为 true
MapTypescala.collection.MapMapType(keyType, valueType, [valueContainsNull])
注意:valueContainsNull 的默认值为 true
StructTypeorg.apache.spark.sql.RowStructType(fields)
注意:fieldsStructField 的一个序列。此外,不允许有两个同名字段。
StructField此字段数据类型的 Scala 值类型
(例如,对于数据类型为 IntegerTypeStructField,值为 Int
StructField(name, dataType, [nullable])
注意:nullable 的默认值为 true

Java

Spark SQL 的所有数据类型都位于 org.apache.spark.sql.types 包中。要访问或创建数据类型,请使用 org.apache.spark.sql.types.DataTypes 中提供的工厂方法。

数据类型Java 中的值类型访问或创建数据类型的 API
ByteTypebyteByteDataTypes.ByteType
ShortTypeshortShortDataTypes.ShortType
IntegerTypeintIntegerDataTypes.IntegerType
LongTypelongLongDataTypes.LongType
FloatTypefloatFloatDataTypes.FloatType
DoubleTypedoubleDoubleDataTypes.DoubleType
DecimalTypejava.math.BigDecimalDataTypes.createDecimalType()
DataTypes.createDecimalType(precision, scale)
StringTypeStringDataTypes.StringType
BinaryTypebyte[]DataTypes.BinaryType
BooleanTypebooleanBooleanDataTypes.BooleanType
TimestampTypejava.time.Instantjava.sql.TimestampDataTypes.TimestampType
TimestampNTZTypejava.time.LocalDateTimeDataTypes.TimestampNTZType
DateTypejava.time.LocalDatejava.sql.DateDataTypes.DateType
YearMonthIntervalTypejava.time.PeriodDataTypes.YearMonthIntervalType
DayTimeIntervalTypejava.time.DurationDataTypes.DayTimeIntervalType
ArrayTypejava.util.ListDataTypes.createArrayType(elementType)
注意:containsNull 的值将为 true
DataTypes.createArrayType(elementType, containsNull)
MapTypejava.util.MapDataTypes.createMapType(keyType, valueType)
注意:valueContainsNull 的值将为 true
DataTypes.createMapType(keyType, valueType, valueContainsNull)
StructTypeorg.apache.spark.sql.RowDataTypes.createStructType(fields)
注意:fieldsStructFieldList 或数组。此外,不允许有两个同名字段。
StructField此字段数据类型的 Java 值类型
(例如,对于数据类型为 IntegerTypeStructField,值为 int
DataTypes.createStructField(name, dataType, nullable)

R

数据类型R 中的值类型访问或创建数据类型的 API
ByteTypeinteger
注意:数字将在运行时转换为 1 字节有符号整数。请确保数字在 -128 到 127 的范围内。
"byte"
ShortTypeinteger
注意:数字将在运行时转换为 2 字节有符号整数。请确保数字在 -32768 到 32767 的范围内。
"short"
IntegerTypeinteger"integer"
LongTypeinteger
注意:数字将在运行时转换为 8 字节有符号整数。请确保数字在 -9223372036854775808 到 9223372036854775807 的范围内。否则,请将数据转换为 decimal.Decimal 并使用 DecimalType
"long"
FloatTypenumeric
注意:数字将在运行时转换为 4 字节单精度浮点数。
"float"
DoubleTypenumeric"double"
DecimalType不支持不支持
StringTypecharacter"string"
BinaryTyperaw"binary"
BooleanTypelogical"bool"
TimestampTypePOSIXct"timestamp"
DateTypeDate"date"
ArrayTypevectorlist`list(type=“array”, elementType=elementType, containsNull=[cont
http://www.dtcms.com/a/581115.html

相关文章:

  • 无需 iTunes,将 iPhone 语音备忘录传输到电脑
  • 三个好思路:SQL并行化处理、混淆矩阵和特征交叉
  • 5 种无需 iTunes 将 iPad 照片传输到电脑的方法
  • 网站制作网站设计自助建站网站程序源码
  • Jenkins 定时触发(cron)使用说明
  • Kubernetes 架构
  • 自己做有趣的网站娱乐网站名字
  • 黑马JAVAWeb-09 文件上传-文件存储到服务器本地磁盘-文件存储在阿里云
  • 医疗小程序04添加就诊人
  • uboot下查看分区
  • 微信小程序camera相机帧转图片base64
  • Agentic AI基础设施实践经验系列(四):MCP服务器从本地到云端的部署演进
  • Linux系统性基础学习笔记
  • DDR5 DFE(Decision Feedback Equalizer)
  • 前程无忧企业官方网站logo制作下载
  • 做网站难学吗wordpress替换百度站内搜索
  • STM32项目分享:基于单片机的空气质量检测系统设计
  • Windows 下PostgreSQL 数据库相关及 n8n .env文件的配置
  • jsp与网站开发期末试题做调查问卷赚钱哪个网站好
  • 在Centos7.9上安装配置zabbix proxy保姆级教程
  • 万能近似定理:神经网络「拟合万物」的理论基石
  • autofs自动挂载
  • 微软TinyTroupe“人格”模拟库:AI智能体市场调研-V3版本(五)
  • Opencv(九) : 图像旋转
  • 关键词解释:DAG 系统(Directed Acyclic Graph,有向无环图)
  • 【Linux】基础开发⼯具
  • 那些网站可以给产品做推广个人网站备案填写
  • 现代汽车确认遭遇数据泄露, 攻击者连续窃密9天获取用户驾照信息
  • 如何进行数据脱取
  • 将linux操作系统装入U盘20251107