Apache Spark 2.2.0 中文文档 - Spark SQL, DataFrames and Datasets Guide | ApacheCN
时间:2022-03-15 04:33
Spark SQL, DataFrames and Datasets Guide
Overview
Spark SQL 是 Spark 处理结构化数据的一个模块.与基础的 Spark RDD API 不同, Spark SQL 提供了查询结构化数据及计算结果等信息的接口.在内部, Spark SQL 使用这个额外的信息去执行额外的优化.有几种方式可以跟 Spark SQL 进行交互, 包括 SQL 和 Dataset API.当使用相同执行引擎进行计算时, 无论使用哪种 API / 语言都可以快速的计算.这种统一意味着开发人员能够在基于提供最自然的方式来表达一个给定的 transformation API 之间实现轻松的来回切换不同的 .
该页面所有例子使用的示例数据都包含在 Spark 的发布中, 并且可以使用 spark-shell
, pyspark
shell, 或者 sparkR
shell来运行.
SQL
Spark SQL 的功能之一是执行 SQL 查询.Spark SQL 也能够被用于从已存在的 Hive 环境中读取数据.更多关于如何配置这个特性的信息, 请参考 这部分. 当以另外的编程语言运行SQL 时, 查询结果将以 的形式返回.您也可以使用 或者通过 与 SQL 接口交互.
Datasets and DataFrames
一个 Dataset 是一个分布式的数据集合 Dataset 是在 Spark 1.6 中被添加的新接口, 它提供了 RDD 的优点(强类型化, 能够使用强大的 lambda 函数)与Spark SQL执行引擎的优点.一个 Dataset 可以从 JVM 对象来 并且使用转换功能(map, flatMap, filter, 等等). Dataset API 在 和是可用的.Python 不支持 Dataset API.但是由于 Python 的动态特性, 许多 Dataset API 的优点已经可用了 (也就是说, 你可能通过 name 天生的row.columnName
属性访问一行中的字段).这种情况和 R 相似.
一个 DataFrame 是一个 Dataset 组成的指定列.它的概念与一个在关系型数据库或者在 R/Python 中的表是相等的, 但是有很多优化. DataFrames 可以从大量的 中构造出来, 比如: 结构化的文本文件, Hive中的表, 外部数据库, 或者已经存在的 RDDs. DataFrame API 可以在 Scala, Java, , 和 中实现. 在 Scala 和 Java中, 一个 DataFrame 所代表的是一个多个 Row
(行)的的 Dataset(数据集合). 在 中, DataFrame
仅仅是一个 Dataset[Row]
类型的别名. 然而, 在 中, 用户需要去使用 Dataset<Row>
去代表一个 DataFrame
.
在此文档中, 我们将常常会引用 Scala/Java Datasets 的 Row
s 作为 DataFrames.
开始入门
起始点: SparkSession
Spark SQL中所有功能的入口点是 类. 要创建一个 SparkSession
, 仅使用 SparkSession.builder()
就可以了:
import org.apache.spark.sql.SparkSession
val spark = SparkSession
.builder()
.appName("Spark SQL basic example")
.config("spark.some.config.option", "some-value")
.getOrCreate()
// For implicit conversions like converting RDDs to DataFrames
import spark.implicits._
Spark 2.0 中的SparkSession
为 Hive 特性提供了内嵌的支持, 包括使用 HiveQL 编写查询的能力, 访问 Hive UDF,以及从 Hive 表中读取数据的能力.为了使用这些特性, 你不需要去有一个已存在的 Hive 设置.
创建 DataFrames
在一个 SparkSession
中, 应用程序可以从一个 , 从hive表, 或者从 中创建一个DataFrames.
举个例子, 下面就是基于一个JSON文件创建一个DataFrame:
val df = spark.read.json("examples/src/main/resources/people.json")
// Displays the content of the DataFrame to stdout
df.show()
// +----+-------+
// | age| name|
// +----+-------+
// |null|Michael|
// | 30| Andy|
// | 19| Justin|
// +----+-------+
无类型的Dataset操作 (aka DataFrame 操作)
DataFrames 提供了一个特定的语法用在 , , and 中机构化数据的操作.
正如上面提到的一样, Spark 2.0中, DataFrames在Scala 和 Java API中, 仅仅是多个 Row
s的Dataset. 这些操作也参考了与强类型的Scala/Java Datasets中的”类型转换” 对应的”无类型转换” .
这里包括一些使用 Dataset 进行结构化数据处理的示例 :
// This import is needed to use the $-notation
import spark.implicits._
// Print the schema in a tree format
df.printSchema()
// root
// |-- age: long (nullable = true)
// |-- name: string (nullable = true)
// Select only the "name" column
df.select("name").show()
// +-------+
// | name|
// +-------+
// |Michael|
// | Andy|
// | Justin|
// +-------+
// Select everybody, but increment the age by 1
df.select($"name", $"age" + 1).show()
// +-------+---------+
// | name|(age + 1)|
// +-------+---------+
// |Michael| null|
// | Andy| 31|
// | Justin| 20|
// +-------+---------+
// Select people older than 21
df.filter($"age" > 21).show()
// +---+----+
// |age|name|
// +---+----+
// | 30|Andy|
// +---+----+
// Count people by age
df.groupBy("age").count().show()
// +----+-----+
// | age|count|
// +----+-----+
// | 19| 1|
// |null| 1|
// | 30| 1|
// +----+-----+
能够在 DataFrame 上被执行的操作类型的完整列表请参考 .
除了简单的列引用和表达式之外, DataFrame 也有丰富的函数库, 包括 string 操作, date 算术, 常见的 math 操作以及更多.可用的完整列表请参考 .
Running SQL Queries Programmatically
SparkSession
的 sql
函数可以让应用程序以编程的方式运行 SQL 查询, 并将结果作为一个 DataFrame
返回.
// Register the DataFrame as a SQL temporary view
df.createOrReplaceTempView("people")
val sqlDF = spark.sql("SELECT * FROM people")
sqlDF.show()
// +----+-------+
// | age| name|
// +----+-------+
// |null|Michael|
// | 30| Andy|
// | 19| Justin|
// +----+-------+
全局临时视图
Spark SQL中的临时视图是session级别的, 也就是会随着session的消失而消失. 如果你想让一个临时视图在所有session中相互传递并且可用, 直到Spark 应用退出, 你可以建立一个全局的临时视图.全局的临时视图存在于系统数据库 global_temp
中, 我们必须加上库名去引用它, 比如. SELECT * FROM global_temp.view1
.
// Register the DataFrame as a global temporary view
df.createGlobalTempView("people")
// Global temporary view is tied to a system preserved database `global_temp`
spark.sql("SELECT * FROM global_temp.people").show()
// +----+-------+
// | age| name|
// +----+-------+
// |null|Michael|
// | 30| Andy|
// | 19| Justin|
// +----+-------+
// Global temporary view is cross-session
spark.newSession().sql("SELECT * FROM global_temp.people").show()
// +----+-------+
// | age| name|
// +----+-------+
// |null|Michael|
// | 30| Andy|
// | 19| Justin|
// +----+-------+
创建Datasets
Dataset 与 RDD 相似, 然而, 并不是使用 Java 序列化或者 Kryo 来序列化用于处理或者通过网络进行传输的对象. 虽然编码器和标准的序列化都负责将一个对象序列化成字节, 编码器是动态生成的代码, 并且使用了一种允许 Spark 去执行许多像 filtering, sorting 以及 hashing 这样的操作, 不需要将字节反序列化成对象的格式.
// Note: Case classes in Scala 2.10 can support only up to 22 fields. To work around this limit,
// you can use custom classes that implement the Product interface
case class Person(name: String, age: Long)
// Encoders are created for case classes
val caseClassDS = Seq(Person("Andy", 32)).toDS()
caseClassDS.show()
// +----+---+
// |name|age|
// +----+---+
// |Andy| 32|
// +----+---+
// Encoders for most common types are automatically provided by importing spark.implicits._
val primitiveDS = Seq(1, 2, 3).toDS()
primitiveDS.map(_ + 1).collect() // Returns: Array(2, 3, 4)
// DataFrames can be converted to a Dataset by providing a class. Mapping will be done by name
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|
// +----+-------+
RDD的互操作性
Spark SQL 支持两种不同的方法用于转换已存在的 RDD 成为 Dataset.第一种方法是使用反射去推断一个包含指定的对象类型的 RDD 的 Schema.在你的 Spark 应用程序中当你已知 Schema 时这个基于方法的反射可以让你的代码更简洁.
第二种用于创建 Dataset 的方法是通过一个允许你构造一个 Schema 然后把它应用到一个已存在的 RDD 的编程接口.然而这种方法更繁琐, 当列和它们的类型知道运行时都是未知时它允许你去构造 Dataset.
使用反射推断Schema
Spark SQL 的 Scala 接口支持自动转换一个包含 case classes 的 RDD 为 DataFrame.Case class 定义了表的 Schema.Case class 的参数名使用反射读取并且成为了列名.Case class 也可以是嵌套的或者包含像 Seq
或者 Array
这样的复杂类型.这个 RDD 能够被隐式转换成一个 DataFrame 然后被注册为一个表.表可以用于后续的 SQL 语句.
// For implicit conversions from RDDs to DataFrames
import spark.implicits._
// Create an RDD of Person objects from a text file, convert it to a Dataframe
val peopleDF = spark.sparkContext
.textFile("examples/src/main/resources/people.txt")
.map(_.split(","))
.map(attributes => Person(attributes(0), attributes(1).trim.toInt))
.toDF()
// Register the DataFrame as a temporary view
peopleDF.createOrReplaceTempView("people")
// SQL statements can be run by using the sql methods provided by Spark
val teenagersDF = spark.sql("SELECT name, age FROM people WHERE age BETWEEN 13 AND 19")
// The columns of a row in the result can be accessed by field index
teenagersDF.map(teenager => "Name: " + teenager(0)).show()
// +------------+
// | value|
// +------------+
// |Name: Justin|
// +------------+
// or by field name
teenagersDF.map(teenager => "Name: " + teenager.getAs[String]("name")).show()
// +------------+
// | value|
// +------------+
// |Name: Justin|
// +------------+
// No pre-defined encoders for Dataset[Map[K,V]], define explicitly
implicit val mapEncoder = org.apache.spark.sql.Encoders.kryo[Map[String, Any]]
// Primitive types and case classes can be also defined as
// implicit val stringIntMapEncoder: Encoder[Map[String, Any]] = ExpressionEncoder()
// row.getValuesMap[T] retrieves multiple columns at once into a Map[String, T]
teenagersDF.map(teenager => teenager.getValuesMap[Any](List("name", "age"))).collect()
// Array(Map("name" -> "Justin", "age" -> 19))
以编程的方式指定Schema
当 case class 不能够在执行之前被定义(例如, records 记录的结构在一个 string 字符串中被编码了, 或者一个 text 文本 dataset 将被解析并且不同的用户投影的字段是不一样的).一个 DataFrame
可以使用下面的三步以编程的方式来创建.
- 从原始的 RDD 创建 RDD 的
Row
(行); - Step 1 被创建后, 创建 Schema 表示一个
StructType
匹配 RDD 中的Row
(行)的结构. - 通过
SparkSession
提供的createDataFrame
方法应用 Schema 到 RDD 的 RowS(行).
例如:
import org.apache.spark.sql.types._
// Create an RDD
val peopleRDD = spark.sparkContext.textFile("examples/src/main/resources/people.txt")
// The schema is encoded in a string
val schemaString = "name age"
// Generate the schema based on the string of schema
val fields = schemaString.split(" ")
.map(fieldName => StructField(fieldName, StringType, nullable = true))
val schema = StructType(fields)
// Convert records of the RDD (people) to Rows
val rowRDD = peopleRDD
.map(_.split(","))
.map(attributes => Row(attributes(0), attributes(1).trim))
// Apply the schema to the RDD
val peopleDF = spark.createDataFrame(rowRDD, schema)
// Creates a temporary view using the DataFrame
peopleDF.createOrReplaceTempView("people")
// SQL can be run over a temporary view created using DataFrames
val results = spark.sql("SELECT name FROM people")
// The results of SQL queries are DataFrames and support all the normal RDD operations
// The columns of a row in the result can be accessed by field index or by field name
results.map(attributes => "Name: " + attributes(0)).show()
// +-------------+
// | value|
// +-------------+
// |Name: Michael|
// | Name: Andy|
// | Name: Justin|
// +-------------+
Aggregations
The provide common aggregations such as count()
, countDistinct()
, avg()
, max()
, min()
, etc. While those functions are designed for DataFrames, Spark SQL also has type-safe versions for some of them in and to work with strongly typed Datasets. Moreover, users are not limited to the predefined aggregate functions and can create their own.
Untyped User-Defined Aggregate Functions
Users have to extend the abstract class to implement a custom untyped aggregate function. For example, a user-defined average can look like:
import org.apache.spark.sql.expressions.MutableAggregationBuffer
import org.apache.spark.sql.expressions.UserDefinedAggregateFunction
import org.apache.spark.sql.types._
import org.apache.spark.sql.Row
import org.apache.spark.sql.SparkSession
object MyAverage extends UserDefinedAggregateFunction {
// Data types of input arguments of this aggregate function
def inputSchema: StructType = StructType(StructField("inputColumn", LongType) :: Nil)
// Data types of values in the aggregation buffer
def bufferSchema: StructType = {
StructType(StructField("sum", LongType) :: StructField("count", LongType) :: Nil)
}
// The data type of the returned value
def dataType: DataType = DoubleType
// Whether this function always returns the same output on the identical input
def deterministic: Boolean = true
// Initializes the given aggregation buffer. The buffer itself is a `Row` that in addition to
// standard methods like retrieving a value at an index (e.g., get(), getBoolean()), provides
// the opportunity to update its values. Note that arrays and maps inside the buffer are still
// immutable.
def initialize(buffer: MutableAggregationBuffer): Unit = {
buffer(0) = 0L
buffer(1) = 0L
}
// Updates the given aggregation buffer `buffer` with new input data from `input`
def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
if (!input.isNullAt(0)) {
buffer(0) = buffer.getLong(0) + input.getLong(0)
buffer(1) = buffer.getLong(1) + 1
}
}
// Merges two aggregation buffers and stores the updated buffer values back to `buffer1`
def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
buffer1(0) = buffer1.getLong(0) + buffer2.getLong(0)
buffer1(1) = buffer1.getLong(1) + buffer2.getLong(1)
}
// Calculates the final result
def evaluate(buffer: Row): Double = buffer.getLong(0).toDouble / buffer.getLong(1)
}
// Register the function to access it
spark.udf.register("myAverage", MyAverage)
val df = spark.read.json("examples/src/main/resources/employees.json")
df.createOrReplaceTempView("employees")
df.show()
// +-------+------+
// | name|salary|
// +-------+------+
// |Michael| 3000|
// | Andy| 4500|
// | Justin| 3500|
// | Berta| 4000|
// +-------+------+
val result = spark.sql("SELECT myAverage(salary) as average_salary FROM employees")
result.show()
// +--------------+
// |average_salary|
// +--------------+
// | 3750.0|
// +--------------+
Type-Safe User-Defined Aggregate Functions
User-defined aggregations for strongly typed Datasets revolve around the abstract class. For example, a type-safe user-defined average can look like:
import org.apache.spark.sql.expressions.Aggregator
import org.apache.spark.sql.Encoder
import org.apache.spark.sql.Encoders
import org.apache.spark.sql.SparkSession
case class Employee(name: String, salary: Long)
case class Average(var sum: Long, var count: Long)
object MyAverage extends Aggregator[Employee, Average, Double] {
// A zero value for this aggregation. Should satisfy the property that any b + zero = b
def zero: Average = Average(0L, 0L)
// Combine two values to produce a new value. For performance, the function may modify `buffer`
// and return it instead of constructing a new object
def reduce(buffer: Average, employee: Employee): Average = {
buffer.sum += employee.salary
buffer.count += 1
buffer
}
// Merge two intermediate values
def merge(b1: Average, b2: Average): Average = {
b1.sum += b2.sum
b1.count += b2.count
b1
}
// Transform the output of the reduction
def finish(reduction: Average): Double = reduction.sum.toDouble / reduction.count
// Specifies the Encoder for the intermediate value type
def bufferEncoder: Encoder[Average] = Encoders.product
// Specifies the Encoder for the final output value type
def outputEncoder: Encoder[Double] = Encoders.scalaDouble
}
val ds = spark.read.json("examples/src/main/resources/employees.json").as[Employee]
ds.show()
// +-------+------+
// | name|salary|
// +-------+------+
// |Michael| 3000|
// | Andy| 4500|
// | Justin| 3500|
// | Berta| 4000|
// +-------+------+
// Convert the function to a `TypedColumn` and give it a name
val averageSalary = MyAverage.toColumn.name("average_salary")
val result = ds.select(averageSalary)
result.show()
// +--------------+
// |average_salary|
// +--------------+
// | 3750.0|
// +--------------+
Data Sources (数据源)
Spark SQL 支持通过 DataFrame 接口对各种 data sources (数据源)进行操作. DataFrame 可以使用 relational transformations (关系转换)操作, 也可用于创建 temporary view (临时视图). 将 DataFrame 注册为 temporary view (临时视图)允许您对其数据运行 SQL 查询. 本节 描述了使用 Spark Data Sources 加载和保存数据的一般方法, 然后涉及可用于 built-in data sources (内置数据源)的 specific options (特定选项).
Generic Load/Save Functions (通用 加载/保存 功能)
在最简单的形式中, 默认数据源(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")
Manually Specifying Options (手动指定选项)
您还可以 manually specify (手动指定)将与任何你想传递给 data source 的其他选项一起使用的 data source . Data sources 由其 fully qualified name (完全限定名称)(即 org.apache.spark.sql.parquet
), 但是对于 built-in sources (内置的源), 你也可以使用它们的 shortnames (短名称)(json
, parquet
, jdbc
, orc
, libsvm
, csv
, text
).从任何 data source type (数据源类型)加载 DataFrames 可以使用此 syntax (语法)转换为其他类型.
val peopleDF = spark.read.format("json").load("examples/src/main/resources/people.json")
peopleDF.select("name", "age").write.format("parquet").save("namesAndAges.parquet")
Run SQL on files directly (直接在文件上运行 SQL)
不使用读取 API 将文件加载到 DataFrame 并进行查询, 也可以直接用 SQL 查询该文件.
val sqlDF = spark.sql("SELECT * FROM parquet.`examples/src/main/resources/users.parquet`")
Save Modes (保存模式)
Save operations (保存操作)可以选择使用 SaveMode
, 它指定如何处理现有数据如果存在的话. 重要的是要意识到, 这些 save modes (保存模式)不使用任何 locking (锁定)并且不是 atomic (原子). 另外, 当执行 Overwrite
时, 数据将在新数据写出之前被删除.
Scala/Java | Any Language | Meaning |
---|---|---|
SaveMode.ErrorIfExists (default) |
"error" (default) |
将 DataFrame 保存到 data source (数据源)时, 如果数据已经存在, 则会抛出异常. |
SaveMode.Append |
"append" |
将 DataFrame 保存到 data source (数据源)时, 如果 data/table 已存在, 则 DataFrame 的内容将被 append (附加)到现有数据中. |
SaveMode.Overwrite |
"overwrite" |
Overwrite mode (覆盖模式)意味着将 DataFrame 保存到 data source (数据源)时, 如果 data/table 已经存在, 则预期 DataFrame 的内容将 overwritten (覆盖)现有数据. |
SaveMode.Ignore |
"ignore" |
Ignore mode (忽略模式)意味着当将 DataFrame 保存到 data source (数据源)时, 如果数据已经存在, 则保存操作预期不会保存 DataFrame 的内容, 并且不更改现有数据. 这与 SQL 中的 CREATE TABLE IF NOT EXISTS 类似. |
Saving to Persistent Tables (保存到持久表)
DataFrames
也可以使用 saveAsTable
命令作为 persistent tables (持久表)保存到 Hive metastore 中. 请注意, existing Hive deployment (现有的 Hive 部署)不需要使用此功能. Spark 将为您创建默认的 local Hive metastore (本地 Hive metastore)(使用 Derby ). 与 createOrReplaceTempView
命令不同, saveAsTable
将 materialize (实现) DataFrame 的内容, 并创建一个指向 Hive metastore 中数据的指针. 即使您的 Spark 程序重新启动, Persistent tables (持久性表)仍然存在, 因为您保持与同一个 metastore 的连接. 可以通过使用表的名称在 SparkSession
上调用 table
方法来创建 persistent tabl (持久表)的 DataFrame .
对于 file-based (基于文件)的 data source (数据源), 例如 text, parquet, json等, 您可以通过 path
选项指定 custom table path (自定义表路径), 例如 df.write.option("path", "/some/path").saveAsTable("t")
. 当表被 dropped (删除)时, custom table path (自定义表路径)将不会被删除, 并且表数据仍然存在. 如果未指定自定义表路径, Spark 将把数据写入 warehouse directory (仓库目录)下的默认表路径. 当表被删除时, 默认的表路径也将被删除.
从 Spark 2.1 开始, persistent datasource tables (持久性数据源表)将 per-partition metadata (每个分区元数据)存储在 Hive metastore 中. 这带来了几个好处:
- 由于 metastore 只能返回查询的必要 partitions (分区), 因此不再需要将第一个查询上的所有 partitions discovering 到表中.
- Hive DDLs 如
ALTER TABLE PARTITION ... SET LOCATION
现在可用于使用 Datasource API 创建的表.
请注意, 创建 external datasource tables (外部数据源表)(带有 path
选项)的表时, 默认情况下不会收集 partition information (分区信息). 要 sync (同步) metastore 中的分区信息, 可以调用 MSCK REPAIR TABLE
.
Bucketing, Sorting and Partitioning (分桶, 排序和分区)
对于 file-based data source (基于文件的数据源), 也可以对 output (输出)进行 bucket 和 sort 或者 partition . Bucketing 和 sorting 仅适用于 persistent tables :
peopleDF.write.bucketBy(42, "name").sortBy("age").saveAsTable("people_bucketed")
在使用 Dataset API 时, partitioning 可以同时与 save
和 saveAsTable
一起使用.
usersDF.write.partitionBy("favorite_color").format("parquet").save("namesPartByColor.parquet")
可以为 single table (单个表)使用 partitioning 和 bucketing:
peopleDF
.write
.partitionBy("favorite_color")
.bucketBy(42, "name")
.saveAsTable("people_partitioned_bucketed")
partitionBy
创建一个 directory structure (目录结构), 如 部分所述. 因此, 对 cardinality (基数)较高的 columns 的适用性有限. 相反, bucketBy
可以在固定数量的 buckets 中分配数据, 并且可以在 a number of unique values is unbounded (多个唯一值无界时)使用数据.
Parquet Files
是许多其他数据处理系统支持的 columnar format (柱状格式). Spark SQL 支持读写 Parquet 文件, 可自动保留 schema of the original data (原始数据的模式). 当编写 Parquet 文件时, 出于兼容性原因, 所有 columns 都将自动转换为可空.
Loading Data Programmatically (以编程的方式加载数据)
使用上面例子中的数据:
// Encoders for most common types are automatically provided by importing spark.implicits._
import spark.implicits._
val peopleDF = spark.read.json("examples/src/main/resources/people.json")
// DataFrames can be saved as Parquet files, maintaining the schema information
peopleDF.write.parquet("people.parquet")
// Read in the parquet file created above
// Parquet files are self-describing so the schema is preserved
// The result of loading a Parquet file is also a DataFrame
val parquetFileDF = spark.read.parquet("people.parquet")
// Parquet files can also be used to create a temporary view and then used in SQL statements
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|
// +------------+
Partition Discovery (分区发现)
Table partitioning (表分区)是在像 Hive 这样的系统中使用的常见的优化方法. 在 partitioned table (分区表)中, 数据通常存储在不同的目录中, partitioning column values encoded (分区列值编码)在每个 partition directory (分区目录)的路径中. Parquet data source (Parquet 数据源)现在可以自动 discover (发现)和 infer (推断)分区信息. 例如, 我们可以使用以下 directory structure (目录结构)将所有以前使用的 population data (人口数据)存储到 partitioned table (分区表)中, 其中有两个额外的列 gender
和 country
作为 partitioning columns (分区列):
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.parquet
或 SparkSession.read.load
, Spark SQL 将自动从路径中提取 partitioning information (分区信息). 现在返回的 DataFrame 的 schema (模式)变成:
root
|-- name: string (nullable = true)
|-- age: long (nullable = true)
|-- gender: string (nullable = true)
|-- country: string (nullable = true)
请注意, 会自动 inferred (推断) partitioning columns (分区列)的 data types (数据类型).目前, 支持 numeric data types (数字数据类型)和 string type (字符串类型).有些用户可能不想自动推断 partitioning columns (分区列)的数据类型.对于这些用例, automatic type inference (自动类型推断)可以由 spark.sql.sources.partitionColumnTypeInference.enabled
配置, 默认为 true
.当禁用 type inference (类型推断)时, string type (字符串类型)将用于 partitioning columns (分区列).
从 Spark 1.6.0 开始, 默认情况下, partition discovery (分区发现)只能找到给定路径下的 partitions (分区).对于上述示例, 如果用户将 path/to/table/gender=male
传递给 SparkSession.read.parquet
或 SparkSession.read.load
, 则 gender
将不被视为 partitioning column (分区列).如果用户需要指定 partition discovery (分区发现)应该开始的基本路径, 则可以在数据源选项中设置 basePath
.例如, 当 path/to/table/gender=male
是数据的路径并且用户将 basePath
设置为 path/to/table/
, gender
将是一个 partitioning column (分区列).
Schema Merging (模式合并)
像 ProtocolBuffer , Avro 和 Thrift 一样, Parquet 也支持 schema evolution (模式演进). 用户可以从一个 simple schema (简单的架构)开始, 并根据需要逐渐向 schema 添加更多的 columns (列). 以这种方式, 用户可能会使用不同但相互兼容的 schemas 的 multiple Parquet files (多个 Parquet 文件). Parquet data source (Parquet 数据源)现在能够自动检测这种情况并 merge (合并)所有这些文件的 schemas .
由于 schema merging (模式合并)是一个 expensive operation (相对昂贵的操作), 并且在大多数情况下不是必需的, 所以默认情况下从 1.5.0 开始. 你可以按照如下的方式启用它:
- 读取 Parquet 文件时, 将 data source option (数据源选项)
mergeSchema
设置为true
(如下面的例子所示), 或 - 将 global SQL option (全局 SQL 选项)
spark.sql.parquet.mergeSchema
设置为true
.
// This is used to implicitly convert an RDD to a DataFrame.
import spark.implicits._
// Create a simple DataFrame, store into a partition directory
val squaresDF = spark.sparkContext.makeRDD(1 to 5).map(i => (i, i * i)).toDF("value", "square")
squaresDF.write.parquet("data/test_table/key=1")
// Create another DataFrame in a new partition directory,
// adding a new column and dropping an existing column
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")
// Read the partitioned table
val mergedDF = spark.read.option("mergeSchema", "true").parquet("data/test_table")
mergedDF.printSchema()
// The final schema consists of all 3 columns in the Parquet files together
// with the partitioning column appeared in the partition directory paths
// root
// |-- value: int (nullable = true)
// |-- square: int (nullable = true)
// |-- cube: int (nullable = true)
// |-- key: int (nullable = true)
Hive metastore Parquet table conversion (Hive metastore Parquet table 转换)
当读取和写入 Hive metastore Parquet 表时, Spark SQL 将尝试使用自己的 Parquet support (Parquet 支持), 而不是 Hive SerDe 来获得更好的性能. 此 behavior (行为)由 spark.sql.hive.convertMetastoreParquet
配置控制, 默认情况下 turned on (打开).
Hive/Parquet Schema Reconciliation
从 table schema processing (表格模式处理)的角度来说, Hive 和 Parquet 之间有两个关键的区别.
- Hive 不区分大小写, 而 Parquet 不是
- Hive 认为所有 columns (列)都可以为空, 而 Parquet 中的可空性是 significant (重要)的.
由于这个原因, 当将 Hive metastore Parquet 表转换为 Spark SQL Parquet 表时, 我们必须调整 metastore schema 与 Parquet schema. reconciliation 规则是:
-
在两个 schema 中具有 same name (相同名称)的 Fields (字段)必须具有 same data type (相同的数据类型), 而不管 nullability (可空性). reconciled field 应具有 Parquet 的数据类型, 以便 nullability (可空性)得到尊重.
-
reconciled schema (调和模式)正好包含 Hive metastore schema 中定义的那些字段.
- 只出现在 Parquet schema 中的任何字段将被 dropped (删除)在 reconciled schema 中.
- 仅在 Hive metastore schema 中出现的任何字段在 reconciled schema 中作为 nullable field (可空字段)添加.
Metadata Refreshing (元数据刷新)
Spark SQL 缓存 Parquet metadata 以获得更好的性能. 当启用 Hive metastore Parquet table conversion (转换)时, 这些 converted tables (转换表)的 metadata (元数据)也被 cached (缓存). 如果这些表由 Hive 或其他外部工具更新, 则需要手动刷新以确保 consistent metadata (一致的元数据).
// spark is an existing SparkSession
spark.catalog.refreshTable("my_table")
Configuration (配置)
可以使用 SparkSession
上的 setConf
方法或使用 SQL 运行 SET key = value
命令来完成 Parquet 的配置.
Property Name (参数名称) | Default(默认) | Meaning(含义) |
---|---|---|
spark.sql.parquet.binaryAsString |
false | 一些其他 Parquet-producing systems (Parquet 生产系统), 特别是 Impala, Hive 和旧版本的 Spark SQL , 在 writing out (写出) Parquet schema 时, 不区分 binary data (二进制数据)和 strings (字符串). 该 flag 告诉 Spark SQL 将 binary data (二进制数据)解释为 string (字符串)以提供与这些系统的兼容性. |
spark.sql.parquet.int96AsTimestamp |
true | 一些 Parquet-producing systems , 特别是 Impala 和 Hive , 将 Timestamp 存入INT96 . 该 flag 告诉 Spark SQL 将 INT96 数据解析为 timestamp 以提供与这些系统的兼容性. |
spark.sql.parquet.cacheMetadata |
true | 打开 Parquet schema metadata 的缓存. 可以加快查询静态数据. |
spark.sql.parquet.compression.codec |
snappy | 在编写 Parquet 文件时设置 compression codec (压缩编解码器)的使用. 可接受的值包括: uncompressed, snappy, gzip, lzo . |
spark.sql.parquet.filterPushdown |
true | 设置为 true 时启用 Parquet filter push-down optimization . |
spark.sql.hive.convertMetastoreParquet |
true | 当设置为 false 时, Spark SQL 将使用 Hive SerDe 作为 parquet tables , 而不是内置的支持. |
spark.sql.parquet.mergeSchema |
false |
当为 true 时, Parquet data source (Parquet 数据源) merges (合并)从所有 data files (数据文件)收集的 schemas , 否则如果没有可用的 summary file , 则从 summary file 或 random data file 中挑选 schema . |
spark.sql.optimizer.metadataOnly |
true |
如果为 true , 则启用使用表的 metadata 的 metadata-only query optimization 来生成 partition columns (分区列)而不是 table scans (表扫描). 当 scanned (扫描)的所有 columns (列)都是 partition columns (分区列)并且 query (查询)具有满足 distinct semantics (不同语义)的 aggregate operator (聚合运算符)时, 它将适用. |
JSON Datasets (JSON 数据集)
Spark SQL 可以 automatically infer (自动推断)JSON dataset 的 schema, 并将其作为 Dataset[Row]
加载. 这个 conversion (转换)可以在 Dataset[String]
上使用 SparkSession.read.json()
来完成, 或 JSON 文件.
请注意, 以 a json file 提供的文件不是典型的 JSON 文件. 每行必须包含一个 separate (单独的), self-contained valid (独立的有效的)JSON 对象. 有关更多信息, 请参阅 .
对于 regular multi-line JSON file (常规的多行 JSON 文件), 将 multiLine
选项设置为 true
.
// Primitive types (Int, String, etc) and Product types (case classes) encoders are
// supported by importing this when creating a Dataset.
import spark.implicits._
// A JSON dataset is pointed to by path.
// The path can be either a single text file or a directory storing text files
val path = "examples/src/main/resources/people.json"
val peopleDF = spark.read.json(path)
// The inferred schema can be visualized using the printSchema() method
peopleDF.printSchema()
// root
// |-- age: long (nullable = true)
// |-- name: string (nullable = true)
// Creates a temporary view using the DataFrame
peopleDF.createOrReplaceTempView("people")
// SQL statements can be run by using the sql methods provided by spark
val teenagerNamesDF = spark.sql("SELECT name FROM people WHERE age BETWEEN 13 AND 19")
teenagerNamesDF.show()
// +------+
// | name|
// +------+
// |Justin|
// +------+
// Alternatively, a DataFrame can be created for a JSON dataset represented by
// a Dataset[String] storing one JSON object per string
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|
// +---------------+----+
Hive 表
Spark SQL 还支持读取和写入存储在 中的数据。 但是,由于 Hive 具有大量依赖关系,因此这些依赖关系不包含在默认 Spark 分发中。 如果在类路径中找到 Hive 依赖项,Spark 将自动加载它们。 请注意,这些 Hive 依赖关系也必须存在于所有工作节点上,因为它们将需要访问 Hive 序列化和反序列化库 (SerDes),以访问存储在 Hive 中的数据。
通过将 hive-site.xml
, core-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
目录 开始了 请注意,自从2.0.0以来,hive-site.xml
中的 hive.metastore.warehouse.dir
属性已被弃用。 而是使用 spark.sql.warehouse.dir
来指定仓库中数据库的默认位置。 您可能需要向启动 Spark 应用程序的用户授予写权限。?
import java.io.File
import org.apache.spark.sql.Row
import org.apache.spark.sql.SparkSession
case class Record(key: Int, value: String)
// warehouseLocation points to the default location for managed databases and tables
val warehouseLocation = new File("spark-warehouse").getAbsolutePath
val spark = SparkSession
.builder()
.appName("Spark Hive Example")
.config("spark.sql.warehouse.dir", warehouseLocation)
.enableHiveSupport()
.getOrCreate()
import spark.implicits._
import spark.sql
sql("CREATE TABLE IF NOT EXISTS src (key INT, value STRING) USING hive")
sql("LOAD DATA LOCAL INPATH ‘examples/src/main/resources/kv1.txt‘ INTO TABLE src")
// Queries are expressed in HiveQL
sql("SELECT * FROM src").show()
// +---+-------+
// |key| value|
// +---+-------+
// |238|val_238|
// | 86| val_86|
// |311|val_311|
// ...
// Aggregation queries are also supported.
sql("SELECT COUNT(*) FROM src").show()
// +--------+
// |count(1)|
// +--------+
// | 500 |
// +--------+
// The results of SQL queries are themselves DataFrames and support all normal functions.
val sqlDF = sql("SELECT key, value FROM src WHERE key < 10 ORDER BY key")
// The items in DataFrames are of type Row, which allows you to access each column by ordinal.
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|
// ...
// You can also use DataFrames to create temporary views within a SparkSession.
val recordsDF = spark.createDataFrame((1 to 100).map(i => Record(i, s"val_$i")))
recordsDF.createOrReplaceTempView("records")
// Queries can then join DataFrame data with data stored in 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 表的存储格式
创建 Hive 表时,需要定义如何 从/向 文件系统 read/write 数据,即 “输入格式” 和 “输出格式”。 您还需要定义该表如何将数据反序列化为行,或将行序列化为数据,即 “serde”。 以下选项可用于指定存储格式 (“serde”, “input format”, “output format”),例如,CREATE TABLE src(id int) USING hive OPTIONS(fileFormat ‘parquet‘)
。 默认情况下,我们将以纯文本形式读取表格文件。 请注意,Hive 存储处理程序在创建表时不受支持,您可以使用 Hive 端的存储处理程序创建一个表,并使用 Spark SQL 来读取它。
Property Name | Meaning |
---|---|
fileFormat |
fileFormat是一种存储格式规范的包,包括 "serde","input format" 和 "output format"。 目前我们支持6个文件格式:‘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个文件格式的这个选项。 |
fieldDelim, escapeDelim, collectionDelim, mapkeyDelim, lineDelim |
这些选项只能与 "textfile" 文件格式一起使用。它们定义如何将分隔的文件读入行。 |
使用 OPTIONS
定义的所有其他属性将被视为 Hive serde 属性。
与不同版本的 Hive Metastore 进行交互
Spark SQL 的 Hive 支持的最重要的部分之一是与 Hive metastore 进行交互,这使得 Spark SQL 能够访问 Hive 表的元数据。 从 Spark 1.4.0 开始,使用 Spark SQL 的单一二进制构建可以使用下面所述的配置来查询不同版本的 Hive 转移。 请注意,独立于用于与转移点通信的 Hive 版本,内部 Spark SQL 将针对 Hive 1.2.1 进行编译,并使用这些类进行内部执行(serdes,UDF,UDAF等)。
以下选项可用于配置用于检索元数据的 Hive 版本:
属性名称 | 默认值 | 含义 |
---|---|---|
spark.sql.hive.metastore.version |
1.2.1 |
Hive metastore 版本。 可用选项为 0.12.0 至 1.2.1 。 |
spark.sql.hive.metastore.jars |
builtin |
当启用 -Phive 时,使用 Hive 1.2.1,它与 Spark 程序集捆绑在一起。选择此选项时,spark.sql.hive.metastore.version 必须为 1.2.1 或未定义。 行家 使用从Maven存储库下载的指定版本的Hive jar。 通常不建议在生产部署中使用此配置。 ***** 应用于实例化 HiveMetastoreClient 的 jar 的位置。该属性可以是三个选项之一:
|
spark.sql.hive.metastore.sharedPrefixes |
com.mysql.jdbc, |
使用逗号分隔的类前缀列表,应使用在 Spark SQL 和特定版本的 Hive 之间共享的类加载器来加载。 一个共享类的示例就是用来访问 Hive metastore 的 JDBC driver。 其它需要共享的类,是需要与已经共享的类进行交互的。 例如,log4j 使用的自定义 appender。 |
spark.sql.hive.metastore.barrierPrefixes |
(empty) |
一个逗号分隔的类前缀列表,应该明确地为 Spark SQL 正在通信的 Hive 的每个版本重新加载。 例如,在通常将被共享的前缀中声明的 Hive UDF (即: |
JDBC 连接其它数据库
Spark SQL 还包括可以使用 JDBC 从其他数据库读取数据的数据源。此功能应优于使用 。 这是因为结果作为 DataFrame 返回,并且可以轻松地在 Spark SQL 中处理或与其他数据源连接。 JDBC 数据源也更容易从 Java 或 Python 使用,因为它不需要用户提供 ClassTag。(请注意,这不同于 Spark SQL JDBC 服务器,允许其他应用程序使用 Spark SQL 运行查询)。
要开始使用,您需要在 Spark 类路径中包含特定数据库的 JDBC driver 程序。 例如,要从 Spark Shell 连接到 postgres,您将运行以下命令:
bin/spark-shell --driver-class-path postgresql-9.4.1207.jar --jars postgresql-9.4.1207.jar
可以使用 Data Sources API 将来自远程数据库的表作为 DataFrame 或 Spark SQL 临时视图进行加载。 用户可以在数据源选项中指定 JDBC 连接属性。用户
和 密码
通常作为登录数据源的连接属性提供。 除了连接属性外,Spark 还支持以下不区分大小写的选项:
属性名称 | 含义 |
---|---|
url |
要连接的JDBC URL。 源特定的连接属性可以在URL中指定。 例如jdbc:jdbc:postgresql://localhost/test?user=fred&password=secret |
dbtable |
应该读取的 JDBC 表。请注意,可以使用在SQL查询的 FROM 子句中有效的任何内容。 例如,您可以使用括号中的子查询代替完整表。 |
driver |
用于连接到此 URL 的 JDBC driver 程序的类名。 |
partitionColumn, lowerBound, upperBound |
如果指定了这些选项,则必须指定这些选项。 另外,必须指定 numPartitions . 他们描述如何从多个 worker 并行读取数据时将表给分区。partitionColumn 必须是有问题的表中的数字列。 请注意,lowerBound 和 upperBound 仅用于决定分区的大小,而不是用于过滤表中的行。 因此,表中的所有行将被分区并返回。此选项仅适用于读操作。 |
numPartitions |
在表读写中可以用于并行度的最大分区数。这也确定并发JDBC连接的最大数量。 如果要写入的分区数超过此限制,则在写入之前通过调用 coalesce(numPartitions) 将其减少到此限制。 |
fetchsize |
JDBC 抓取的大小,用于确定每次数据往返传递的行数。 这有利于提升 JDBC driver 的性能,它们的默认值较小(例如: Oracle 是 10 行)。 该选项仅适用于读取操作。 |
batchsize |
JDBC 批处理的大小,用于确定每次数据往返传递的行数。 这有利于提升 JDBC driver 的性能。 该选项仅适用于写操作。默认值为 1000 . |
isolationLevel |
事务隔离级别,适用于当前连接。 它可以是 NONE , READ_COMMITTED , READ_UNCOMMITTED , REPEATABLE_READ , 或 SERIALIZABLE 之一,对应于 JDBC 连接对象定义的标准事务隔离级别,默认为 READ_UNCOMMITTED 。 此选项仅适用于写操作。请参考 java.sql.Connection 中的文档。 |
truncate |
这是一个与 JDBC 相关的选项。 启用 SaveMode.Overwrite 时,此选项会导致 Spark 截断现有表,而不是删除并重新创建。 这可以更有效,并且防止表元数据(例如,索引)被移除。 但是,在某些情况下,例如当新数据具有不同的模式时,它将无法工作。 它默认为 false 。 此选项仅适用于写操作。 |
createTableOptions |
这是一个与JDBC相关的选项。 如果指定,此选项允许在创建表时设置特定于数据库的表和分区选项(例如:CREATE TABLE t (name string) ENGINE=InnoDB. )。此选项仅适用于写操作。 |
createTableColumnTypes |
使用数据库列数据类型而不是默认值,创建表时。 数据类型信息应以与 CREATE TABLE 列语法相同的格式指定(例如:"name CHAR(64), comments VARCHAR(1024)" )。 指定的类型应该是有效的 spark sql 数据类型。此选项仅适用于写操作。 |
// Note: JDBC loading and saving can be achieved via either the load/save or jdbc methods
// Loading data from a JDBC source
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)
// Saving data to a JDBC source
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)
// Specifying create table column data types on write
jdbcDF.write
.option("createTableColumnTypes", "name CHAR(64), comments VARCHAR(1024)")
.jdbc("jdbc:postgresql:dbserver", "schema.tablename", connectionProperties)
故障排除
- JDBC driver 程序类必须对客户端会话和所有执行程序上的原始类加载器可见。 这是因为 Java 的 DriverManager 类执行安全检查,导致它忽略原始类加载器不可见的所有 driver 程序,当打开连接时。一个方便的方法是修改所有工作节点上的compute_classpath.sh 以包含您的 driver 程序 JAR。
- 一些数据库,例如 H2,将所有名称转换为大写。 您需要使用大写字母来引用 Spark SQL 中的这些名称。
性能调优
对于某些工作负载,可以通