Spark SQL, DataFrames and Datasets Guide
本篇编程语言以java为主,其他请参考:官方文档 Spark SQL
1.概述
Spark SQL是用于结构化数据处理的Spark模块。与基本的Spark RDD API不同,Spark SQL提供的接口为Spark提供了有关数据结构和正在执行的计算的更多信息。在内部,Spark SQL使用此额外信息来执行额外的优化。有几种与Spark SQL交互的方法,包括SQL和Dataset API。在计算结果时,使用相同的执行引擎,与您用于表达计算的API /语言无关。这种统一意味着开发人员可以轻松地在不同的API之间来回切换,从而提供表达给定转换的最自然的方式。
所有的在这个页使用的样本数据的示例中包括在所述火花分布,并且可以在运行spark-shell
, pyspark shell,
sparkR
shell
.
1.1.SQL
Spark SQL的一个用途是执行SQL查询。Spark SQL还可用于从现有Hive安装中读取数据。有关如何配置此功能的更多信息,请参阅Hive Tables部分。从其他编程语言中运行SQL时,结果将作为数据集/数据框返回。您还可以使用命令行 或JDBC / ODBC与SQL接口进行交互。
1.2.数据集和数据框架
数据集是分布式数据集合。数据集是Spark 1.6中添加的一个新接口,它提供了RDD的优势(强类型,使用强大的lambda函数的能力)和Spark SQL优化执行引擎的优点。数据集可以被构造从JVM对象,然后使用功能性的转换(操作map
,flatMap
,filter
等等)。数据集API在Scala和 Java中可用。Python没有对Dataset API的支持。但由于Python的动态特性,数据集API的许多好处已经可用(即您可以自然地按名称访问行的字段 row.columnName
)。R的情况类似。
DataFrame是一个组织成命名列的数据集。它在概念上等同于关系数据库中的表或R / Python中的数据框,但在底层具有更丰富的优化。DataFrame可以从多种来源构建,例如:结构化数据文件,Hive中的表,外部数据库或现有RDD。DataFrame API在Scala,Java,Python和R中可用。在Scala和Java中,DataFrame由Row
s 的数据集表示。在Scala API中,DataFrame
它只是一个类型别名Dataset[Row]
。而在Java API中,用户需要使用Dataset<Row>
来表示DataFrame
。
在本文档中,我们经常将Row
s的Scala / Java数据集称为DataFrame。
2.入门
2.1.起点:SparkSession
Spark中所有功能的入口点都是SparkSession类。要创建基本的SparkSession
,只需使用SparkSession.builder()
:
import org.apache.spark.sql.SparkSession;
SparkSession spark = SparkSession
.builder()
.appName("Java Spark SQL basic example")
.config("spark.some.config.option", "some-value")
.getOrCreate();
在Spark repo中的"examples/src/main/java/org/apache/spark/examples/sql/JavaSparkSQLExample.java"中找到完整的示例代码。
SparkSession
Spark 2.0中提供了对Hive功能的内置支持,包括使用HiveQL编写查询,访问Hive UDF以及从Hive表读取数据的功能。要使用这些功能,您无需拥有现有的Hive设置。
2.2.创建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");
// Displays the content of the DataFrame to stdout
df.show();
// +----+-------+
// | age| name|
// +----+-------+
// |null|Michael|
// | 30| Andy|
// | 19| Justin|
// +----+-------+
在Spark repo中的"examples/src/main/java/org/apache/spark/examples/sql/JavaSparkSQLExample.java"中找到完整的示例代码。
2.3.无类型数据集操作(又名DataFrame操作)
DataFrames为Scala,Java,Python和R中的结构化数据操作提供了特定于域的语言。
如上所述,在Spark 2.0中,DataFrames只是Row
Scala和Java API中的数据集。与“类型转换”相比,这些操作也称为“无类型转换”,带有强类型Scala / Java数据集。
这里我们包括使用数据集进行结构化数据处理的一些基本示例:
// col("...") is preferable to df.col("...")
import static org.apache.spark.sql.functions.col;
// 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(col("name"), col("age").plus(1)).show();
// +-------+---------+
// | name|(age + 1)|
// +-------+---------+
// |Michael| null|
// | Andy| 31|
// | Justin| 20|
// +-------+---------+
// Select people older than 21
df.filter(col("age").gt(21)).show();
// +---+----+
// |age|name|
// +---+----+
// | 30|Andy|
// +---+----+
// Count people by age
df.groupBy("age").count().show();
// +----+-----+
// | age|count|
// +----+-----+
// | 19| 1|
// |null| 1|
// | 30| 1|
// +----+-----+
在Spark repo中的"examples/src/main/java/org/apache/spark/examples/sql/JavaSparkSQLExample.java"中找到完整的示例代码。
有关可在数据集上执行的操作类型的完整列表,请参阅API文档。
除了简单的列引用和表达式之外,数据集还具有丰富的函数库,包括字符串操作,日期算术,常用数学运算等。完整列表可在DataFrame函数参考中找到。
2.4.以编程方式运行SQL查询
该sql
上的功能SparkSession
使应用程序以编程方式运行SQL查询并返回结果的Dataset<Row>
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
// Register the DataFrame as a SQL temporary view
df.createOrReplaceTempView("people");
Dataset<Row> sqlDF = spark.sql("SELECT * FROM people");
sqlDF.show();
// +----+-------+
// | age| name|
// +----+-------+
// |null|Michael|
// | 30| Andy|
// | 19| Justin|
// +----+-------+
在Spark repo中的 "examples/src/main/java/org/apache/spark/examples/sql/JavaSparkSQLExample.java"中找到完整的示例代码。
2.5.全局临时视图
Spark SQL中的临时视图是会话范围的,如果创建它的会话终止,它将消失。如果您希望拥有一个在所有会话之间共享的临时视图并保持活动状态,直到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|
// +----+-------+
2.6.创建数据集
数据集与RDD类似,但是,它们不使用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 int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
// Create an instance of a Bean class
Person person = new Person();
person.setName("Andy");
person.setAge(32);
// Encoders are created for Java beans
Encoder<Person> personEncoder = Encoders.bean(Person.class);
Dataset<Person> javaBeanDS = spark.createDataset(
Collections.singletonList(person),
personEncoder
);
javaBeanDS.show();
// +---+----+
// |age|name|
// +---+----+
// | 32|Andy|
// +---+----+
// Encoders for most common types are provided in class Encoders
Encoder<Integer> integerEncoder = Encoders.INT();
Dataset<Integer> primitiveDS = spark.createDataset(Arrays.asList(1, 2, 3), integerEncoder);
Dataset<Integer> transformedDS = primitiveDS.map(
(MapFunction<Integer, Integer>) value -> value + 1,
integerEncoder);
transformedDS.collect(); // Returns [2, 3, 4]
// DataFrames can be converted to a Dataset by providing a class. Mapping based on name
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|
// +----+-------+
2.7.与RDD互操作
Spark SQL支持两种不同的方法将现有RDD转换为数据集。第一种方法使用反射来推断包含特定类型对象的RDD的模式。这种基于反射的方法可以提供更简洁的代码,并且在您编写Spark应用程序时已经了解模式时可以很好地工作。
创建数据集的第二种方法是通过编程接口,允许您构建模式,然后将其应用于现有RDD。虽然此方法更详细,但它允许您在直到运行时才知道列及其类型时构造数据集。
2.7.1.使用反射推断模式
Spark SQL支持将JavaBeans的RDD自动转换为数据帧(DataFrame)。使用反射获得的beaninfo定义表的模式。目前,Spark SQL不支持包含映射字段的JavaBeans。但是,支持嵌套的JavaBeans和列表或数组字段。您可以通过创建一个实现可序列化的类来创建JavaBean,该类的所有字段都有getter和setter。
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;
// Create an RDD of Person objects from a text file
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;
});
// Apply a schema to an RDD of JavaBeans to get a DataFrame
Dataset<Row> peopleDF = spark.createDataFrame(peopleRDD, Person.class);
// Register the DataFrame as a temporary view
peopleDF.createOrReplaceTempView("people");
// SQL statements can be run by using the sql methods provided by spark
Dataset<Row> teenagersDF = spark.sql("SELECT name FROM people WHERE age BETWEEN 13 AND 19");
// The columns of a row in the result can be accessed by field index
Encoder<String> stringEncoder = Encoders.STRING();
Dataset<String> teenagerNamesByIndexDF = teenagersDF.map(
(MapFunction<Row, String>) row -> "Name: " + row.getString(0),
stringEncoder);
teenagerNamesByIndexDF.show();
// +------------+
// | value|
// +------------+
// |Name: Justin|
// +------------+
// or by field name
Dataset<String> teenagerNamesByFieldDF = teenagersDF.map(
(MapFunction<Row, String>) row -> "Name: " + row.<String>getAs("name"),
stringEncoder);
teenagerNamesByFieldDF.show();
// +------------+
// | value|
// +------------+
// |Name: Justin|
// +------------+
2.7.2.以编程方式指定架构
如果无法提前定义JavaBean类(例如,记录的结构以字符串形式编码,或者文本数据集将被解析,并且字段将针对不同的用户进行不同的投影),Dataset<Row>
则可以通过三个步骤以编程方式创建 。
- 从原始RDD创建
行
的RDD; - 创建由与步骤1中创建的RDD中的行结构匹配的结构类型表示的架构。
通过SparkSession提供的CreateDataFrame方法将架构应用于行的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;
// Create an RDD
JavaRDD<String> peopleRDD = spark.sparkContext()
.textFile("examples/src/main/resources/people.txt", 1)
.toJavaRDD();
// The schema is encoded in a string
String schemaString = "name age";
// Generate the schema based on the string of schema
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);
// Convert records of the RDD (people) to Rows
JavaRDD<Row> rowRDD = peopleRDD.map((Function<String, Row>) record -> {
String[] attributes = record.split(",");
return RowFactory.create(attributes[0], attributes[1].trim());
});
// Apply the schema to the RDD
Dataset<Row> peopleDataFrame = spark.createDataFrame(rowRDD, schema);
// Creates a temporary view using the DataFrame
peopleDataFrame.createOrReplaceTempView("people");
// SQL can be run over a temporary view created using DataFrames
Dataset<Row> 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
Dataset<String> namesDS = results.map(
(MapFunction<Row, String>) row -> "Name: " + row.getString(0),
Encoders.STRING());
namesDS.show();
// +-------------+
// | value|
// +-------------+
// |Name: Michael|
// | Name: Andy|
// | Name: Justin|
// +-------------+
2.8.聚合
该内置功能DataFrames提供共同聚合,例如count()
,countDistinct()
,avg()
,max()
,min()
,等。虽然这些功能是专为DataFrames,Spark SQL还拥有类型安全的版本,在其中的一些 Scala和 Java使用强类型数据集的工作。此外,用户不限于预定义的聚合函数,并且可以创建自己的聚合函数
2.8.1.无用户定义的聚合函数
用户必须扩展UserDefinedAggregateFunction 抽象类以实现自定义无类型聚合函数。例如,用户定义的平均值可能如下所示:
import java.util.ArrayList;
import java.util.List;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
import org.apache.spark.sql.SparkSession;
import org.apache.spark.sql.expressions.MutableAggregationBuffer;
import org.apache.spark.sql.expressions.UserDefinedAggregateFunction;
import org.apache.spark.sql.types.DataType;
import org.apache.spark.sql.types.DataTypes;
import org.apache.spark.sql.types.StructField;
import org.apache.spark.sql.types.StructType;
public static class MyAverage extends UserDefinedAggregateFunction {
private StructType inputSchema;
private StructType bufferSchema;
public MyAverage() {
List<StructField> inputFields = new ArrayList<>();
inputFields.add(DataTypes.createStructField("inputColumn", DataTypes.LongType, true));
inputSchema = DataTypes.createStructType(inputFields);
List<StructField> bufferFields = new ArrayList<>();
bufferFields.add(DataTypes.createStructField("sum", DataTypes.LongType, true));
bufferFields.add(DataTypes.createStructField("count", DataTypes.LongType, true));
bufferSchema = DataTypes.createStructType(bufferFields);
}
// Data types of input arguments of this aggregate function
public StructType inputSchema() {
return inputSchema;
}
// Data types of values in the aggregation buffer
public StructType bufferSchema() {
return bufferSchema;
}
// The data type of the returned value
public DataType dataType() {
return DataTypes.DoubleType;
}
// Whether this function always returns the same output on the identical input
public boolean deterministic() {
return 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.
public void initialize(MutableAggregationBuffer buffer) {
buffer.update(0, 0L);
buffer.update(1, 0L);
}
// Updates the given aggregation buffer `buffer` with new input data from `input`
public void update(MutableAggregationBuffer buffer, Row input) {
if (!input.isNullAt(0)) {
long updatedSum = buffer.getLong(0) + input.getLong(0);
long updatedCount = buffer.getLong(1) + 1;
buffer.update(0, updatedSum);
buffer.update(1, updatedCount);
}
}
// Merges two aggregation buffers and stores the updated buffer values back to `buffer1`
public void merge(MutableAggregationBuffer buffer1, Row buffer2) {
long mergedSum = buffer1.getLong(0) + buffer2.getLong(0);
long mergedCount = buffer1.getLong(1) + buffer2.getLong(1);
buffer1.update(0, mergedSum);
buffer1.update(1, mergedCount);
}
// Calculates the final result
public Double evaluate(Row buffer) {
return ((double) buffer.getLong(0)) / buffer.getLong(1);
}
}
// Register the function to access it
spark.udf().register("myAverage", new MyAverage());
Dataset<Row> 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|
// +-------+------+
Dataset<Row> result = spark.sql("SELECT myAverage(salary) as average_salary FROM employees");
result.show();
// +--------------+
// |average_salary|
// +--------------+
// | 3750.0|
// +--------------+
2.8.2.类型安全的用户定义聚合函数
强类型数据集的用户定义聚合围绕Aggregator抽象类。例如,类型安全的用户定义平均值可能如下所示:
import java.io.Serializable;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Encoder;
import org.apache.spark.sql.Encoders;
import org.apache.spark.sql.SparkSession;
import org.apache.spark.sql.TypedColumn;
import org.apache.spark.sql.expressions.Aggregator;
public static class Employee implements Serializable {
private String name;
private long salary;
// Constructors, getters, setters...
}
public static class Average implements Serializable {
private long sum;
private long count;
// Constructors, getters, setters...
}
public static class MyAverage extends Aggregator<Employee, Average, Double> {
// A zero value for this aggregation. Should satisfy the property that any b + zero = b
public Average zero() {
return new 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
public Average reduce(Average buffer, Employee employee) {
long newSum = buffer.getSum() + employee.getSalary();
long newCount = buffer.getCount() + 1;
buffer.setSum(newSum);
buffer.setCount(newCount);
return buffer;
}
// Merge two intermediate values
public Average merge(Average b1, Average b2) {
long mergedSum = b1.getSum() + b2.getSum();
long mergedCount = b1.getCount() + b2.getCount();
b1.setSum(mergedSum);
b1.setCount(mergedCount);
return b1;
}
// Transform the output of the reduction
public Double finish(Average reduction) {
return ((double) reduction.getSum()) / reduction.getCount();
}
// Specifies the Encoder for the intermediate value type
public Encoder<Average> bufferEncoder() {
return Encoders.bean(Average.class);
}
// Specifies the Encoder for the final output value type
public Encoder<Double> outputEncoder() {
return Encoders.DOUBLE();
}
}
Encoder<Employee> employeeEncoder = Encoders.bean(Employee.class);
String path = "examples/src/main/resources/employees.json";
Dataset<Employee> ds = spark.read().json(path).as(employeeEncoder);
ds.show();
// +-------+------+
// | name|salary|
// +-------+------+
// |Michael| 3000|
// | Andy| 4500|
// | Justin| 3500|
// | Berta| 4000|
// +-------+------+
MyAverage myAverage = new MyAverage();
// Convert the function to a `TypedColumn` and give it a name
TypedColumn<Employee, Double> averageSalary = myAverage.toColumn().name("average_salary");
Dataset<Double> result = ds.select(averageSalary);
result.show();
// +--------------+
// |average_salary|
// +--------------+
// | 3750.0|
// +--------------+
3.数据源
Spark SQL支持通过DataFrame接口对各种数据源进行操作。DataFrame可以使用关系转换进行操作,也可以用于创建临时视图。将DataFrame注册为临时视图允许您对其数据运行SQL查询。
3.1.通用加载/保存功能
在最简单的形式中,默认数据源(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");
"examples/src/main/java/org/apache/spark/examples/sql/JavaSQLDataSourceExample.java"
3.1.1.手动指定选项
您还可以手动指定将要使用的数据源以及要传递给数据源的任何其他选项。数据源通过其全名指定(org.apache.spark.sql.parquet
),但内置的来源,你也可以使用自己的短名称(json
,parquet
,jdbc
,orc
,libsvm
,csv
,text
)。从任何数据源类型加载的DataFrame都可以使用此语法转换为其他类型。
要加载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");
要加载CSV文件,您可以使用:
Dataset<Row> peopleDFCsv = spark.read().format("csv")
.option("sep", ";")
.option("inferSchema", "true")
.option("header", "true")
.load("examples/src/main/resources/people.csv");
在写操作期间也使用额外选项。例如,您可以控制ORC数据源的bloom过滤器和字典编码。下面的例子ORC将在创建布隆过滤器favorite_color
,并使用字典编码name
和favorite_color
。对于Parquet,也存在parquet.enable.dictionary
。要查找有关额外ORC / Parquet选项的更多详细信息,请访问官方Apache ORC / Parquet网站。
usersDF.write().format("orc")
.option("orc.bloom.filter.columns", "favorite_color")
.option("orc.dictionary.key.threshold", "1.0")
.save("users_with_options.orc");
3.1.2.直接在文件上运行SQL
您可以直接使用SQL查询该文件,而不是使用读取API将文件加载到DataFrame并进行查询
Dataset<Row> sqlDF =
spark.sql("SELECT * FROM parquet.`examples/src/main/resources/users.parquet`");
3.1.3.保存模式
保存操作可以选择使用SaveMode
,指定如何处理现有数据(如果存在)。重要的是要意识到这些保存模式不使用任何锁定并且不是原子的。此外,执行时Overwrite
,数据将在写出新数据之前被删除。
Scala/Java | 任何语言 | 含义 |
|
| 将DataFrame保存到数据源时,如果数据已存在,则会引发异常。 |
SaveMode.Append |
| 将DataFrame保存到数据源时,如果数据/表已存在,则DataFrame的内容应附加到现有数据。 |
SaveMode.Overwrite |
| 覆盖模式意味着在将DataFrame保存到数据源时,如果数据/表已经存在,则预期现有数据将被DataFrame的内容覆盖。 |
SaveMode.Ignore |
| 忽略模式意味着在将DataFrame保存到数据源时,如果数据已存在,则预期保存操作不会保存DataFrame的内容而不会更改现有数据。这与 |
3.1.4.保存到持久表
DataFrames
也可以使用该saveAsTable
命令将持久表保存到Hive Metastore中。请注意,使用此功能不需要现有的Hive部署。Spark将为您创建默认的本地Hive Metastore(使用Derby)。与createOrReplaceTempView
命令不同, saveAsTable
将实现DataFrame的内容并创建指向Hive Metastore中数据的指针。只要您保持与同一Metastore的连接,即使您的Spark程序重新启动后,持久表仍然存在。可通过在具有表名称的SparkSession上调用Table方法,可以创建持久表的数据帧。
对于基于文件的数据源,例如text,parquet,json等,您可以通过path
选项指定自定义表路径 ,例如df.write.option("path", "/some/path").saveAsTable("t")
。当删除表时,将不会删除自定义表路径,并且表数据仍然存在。如果未指定自定义表路径,则Spark会将数据写入仓库目录下的默认表路径。当删除表时,也将删除默认表路径。
从Spark 2.1开始,持久数据源表将每个分区元数据存储在Hive Metastore中。这带来了几个好处:
- 由于Metastore只能返回查询所需的分区,因此不再需要在表的第一个查询中发现所有分区。
- Hive DDL
ALTER TABLE PARTITION ... SET LOCATION
现在可用于使用Datasource API创建的表。
请注意,在创建外部数据源表(带有path
选项的表)时,默认情况下不会收集分区信息。要同步Metastore中的分区信息,可以调用MSCK REPAIR TABLE
。
3.1.5.包装、分类和分区(Bucketing,Sorting and Partitioning)
对于基于文件的数据源,还可以对输出进行存储和排序或分区。分段和排序仅适用于持久表:
peopleDF.write().bucketBy(42, "name").sortBy("age").saveAsTable("people_bucketed");
而分区可以既使用save
和saveAsTable
使用DataSet API时。
usersDF
.write()
.partitionBy("favorite_color")
.format("parquet")
.save("namesPartByColor.parquet");
可以对单个表使用分区和分区:
peopleDF
.write()
.partitionBy("favorite_color")
.bucketBy(42, "name")
.saveAsTable("people_partitioned_bucketed");
partitionBy
创建目录结构,如分区发现部分中所述。因此,它对具有高基数的列的适用性有限。相反 bucketBy
,在固定数量的桶中分配数据,并且可以在许多唯一值无限制时使用。
3.2.Parquet Files
Parquet是一种柱状格式,许多其他数据处理系统都支持它。Spark SQL支持读取和写入Parquet文件,这些文件自动保留原始数据的模式。在编写Parquet文件时,出于兼容性原因,所有列都会自动转换为可为空。
3.2.1.以编程方式加载数据
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");
// 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
Dataset<Row> 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");
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|
// +------------+
3.2.2.分区发现
表分区是Hive等系统中常用的优化方法。在分区表中,数据通常存储在不同的目录中,分区列值在每个分区目录的路径中编码。所有内置文件源(包括Text / CSV / JSON / ORC / Parquet)都能够自动发现和推断分区信息。例如,我们可以使用以下目录结构将所有以前使用的填充数据存储到分区表中,使用两个额外的列,gender
并country
作为分区列:
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将自动从路径中提取分区信息。现在返回的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.parquet
或者SparkSession.read.load
,gender
则不会将其视为分区列。如果用户需要指定分区发现应该开始的基本路径,则可以basePath
在数据源选项中进行设置。例如,当path/to/table/gender=male
是设置数据和用户的路径basePath
来path/to/table/
,gender
将是一个分区列。
3.2.3.架构合并
与Protocol Buffer,Avro和Thrift一样,Parquet也支持模式演变。用户可以从简单模式开始,并根据需要逐渐向模式添加更多列。通过这种方式,用户可能最终得到具有不同但相互兼容的模式的多个Parquet文件。Parquet数据源现在能够自动检测这种情况并合并所有这些文件的模式。
由于模式合并是一项相对昂贵的操作,并且在大多数情况下不是必需的,因此我们默认从1.5.0开始关闭它。您可以启用它
- 数据源选项设置
mergeSchema
到true
读取Parquet 文件时(如下),或 - 将全局SQL选项设置
spark.sql.parquet.mergeSchema
为true
。
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;
// Getters and setters...
}
public static class Cube implements Serializable {
private int value;
private int cube;
// Getters and setters...
}
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);
}
// Create a simple DataFrame, store into a partition directory
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);
}
// Create another DataFrame in a new partition directory,
// adding a new column and dropping an existing column
Dataset<Row> cubesDF = spark.createDataFrame(cubes, Cube.class);
cubesDF.write().parquet("data/test_table/key=2");
// Read the partitioned table
Dataset<Row> 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)
3.2.4.Hive Metastore Parquet表转换
在读取和写入Hive Metastore Parquet表时,Spark SQL将尝试使用自己的Parquet支持而不是Hive SerDe来获得更好的性能。此行为由spark.sql.hive.convertMetastoreParquet
配置控制 ,默认情况下处于打开状态。
Hive / Parquet Schema Reconciliation
从表模式处理的角度来看,Hive和Parquet之间存在两个主要区别。
- Hive不区分大小写,而Parquet则不区分大小写
- Hive认为所有列都可以为空,而Parquet中的可空性很重要
由于这个原因,在将Hive Metastore Parquet表转换为Spark SQL Parquet表时,我们必须将Hive Metastore模式与Parquet模式进行协调。对帐规则是:
- 两个模式中具有相同名称的字段必须具有相同的数据类型,而不管是否为空。协调字段应具有Parquet端的数据类型,以便遵循可为空性。
- 协调的模式恰好包含Hive Metastore模式中定义的那些字段。
- 仅出现在Parquet模式中的任何字段都将放入已协调的模式中。
- 仅出现在Hive Metastore模式中的任何字段都将在协调模式中添加为可空字段。
元数据刷新
Spark SQL缓存Parquet元数据以获得更好的性能。启用Hive Metastore Parquet表转换后,还会缓存这些转换表的元数据。如果这些表由Hive或其他外部工具更新,则需要手动刷新它们以确保元数据一致。
// spark is an existing SparkSession
spark.catalog().refreshTable("my_table");
3.2.5.组态
可以使用SQL setConf
上的方法SparkSession
或通过SET key=value
使用SQL 运行 命令来完成Parquet的配置。
属性名称 | 默认 | 含义 |
| false | 其他一些Parquet生成系统,特别是Impala,Hive和旧版本的Spark SQL,在写出Parquet模式时不区分二进制数据和字符串。此标志告诉Spark SQL将二进制数据解释为字符串,以提供与这些系统的兼容性。 |
| true | 一些Parquet生产系统,特别是Impala和Hive,将时间戳存储到INT96中。此标志告诉Spark SQL将INT96数据解释为时间戳,以提供与这些系统的兼容性。 |
| snappy | 设置编写Parquet文件时使用的压缩编解码器。如果在特定于表的选项/属性中指定了“compression”或“parquet.compression”,则优先级为“compression”,“parquet.compression”,“spark.sql.parquet.compression.codec”。可接受的值包括:none,uncompressed,snappy,gzip,lzo,brotli,lz4,zstd。请注意,`zstd`需要在Hadoop 2.9.0之前安装`ZStandardCodec`,`brotli`需要安装`BrotliCodec`。 |
| true | 设置为true时启用Parquet过滤器下推优化。 |
| true | 设置为false时,Spark SQL将使用Hive SerDe作为镶木桌而不是内置支持。 |
| false | |
| false | |
3.3.ORC Files
从Spark 2.3开始,Spark支持带有ORC文件的新ORC文件格式的矢量化ORC阅读器。为此,新添加了以下配置。USING ORC
当spark.sql.orc.impl
设置为native
和spark.sql.orc.enableVectorizedReader
设置为时,矢量化读取器用于本机ORC表(例如,使用子句创建的表)true
。对于Hive ORC serde表(例如,使用该子句创建的表USING HIVE OPTIONS (fileFormat 'ORC')
),当spark.sql.hive.convertMetastoreOrc
也设置为时,使用向量化读取器true
。
属性名称 | 默认 | 含义 |
|
| ORC实现的名称。它可以是 |
|
| 在 |
3.4.JSON Files
Spark SQL可以自动推断JSON数据集的模式并将其加载为Dataset<Row>
。可以使用SparkSession.read().json()
a Dataset<String>
或JSON文件完成此转换。
请注意,作为json文件提供的文件不是典型的JSON文件。每行必须包含一个单独的,自包含的有效JSON对象。有关更多信息,请参阅 JSON Lines文本格式,也称为换行符分隔的JSON。
对于常规多行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|
// +---------------+----+
3.5.Hive Tables
Spark SQL还支持读取和写入存储在Apache Hive中的数据。但是,由于Hive具有大量依赖项,因此这些依赖项不包含在默认的Spark分发中。如果可以在类路径上找到Hive依赖项,Spark将自动加载它们。请注意,这些Hive依赖项也必须存在于所有工作节点上,因为它们需要访问Hive序列化和反序列化库(SerDes)才能访问存储在Hive中的数据。
Hive的结构是通过将您做hive-site.xml
,core-site.xml
(安全性配置),以及hdfs-site.xml
(对于HDFS配置)文件中conf/
。
使用Hive时,必须SparkSession
使用Hive支持进行实例化,包括连接到持久性Hive Metastore,支持Hive serdes和Hive用户定义函数。没有现有Hive部署的用户仍可以启用Hive支持。如果未配置hive-site.xml
,则上下文会自动metastore_db
在当前目录中创建并创建一个配置spark.sql.warehouse.dir
的目录spark-warehouse
,该目录默认为 当前目录中启动Spark应用程序的目录。请注意,自Spark 2.0.0起hive.metastore.warehouse.dir
,hive-site.xml
不推荐使用该属性。而是spark.sql.warehouse.dir
用于指定仓库中数据库的默认位置。您可能需要向启动Spark应用程序的用户授予写入权限。
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 points to the default location for managed databases and tables
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");
// Queries are expressed in HiveQL
spark.sql("SELECT * FROM src").show();
// +---+-------+
// |key| value|
// +---+-------+
// |238|val_238|
// | 86| val_86|
// |311|val_311|
// ...
// Aggregation queries are also supported.
spark.sql("SELECT COUNT(*) FROM src").show();
// +--------+
// |count(1)|
// +--------+
// | 500 |
// +--------+
// The results of SQL queries are themselves DataFrames and support all normal functions.
Dataset<Row> sqlDF = spark.sql("SELECT key, value FROM src WHERE key < 10 ORDER BY key");
// The items in DataFrames are of type Row, which lets you to access each column by ordinal.
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|
// ...
// You can also use DataFrames to create temporary views within a 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");
// Queries can then join DataFrames data with data stored in 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|
// ...
3.5.1.指定Hive表的存储格式
创建Hive表时,需要定义此表应如何从/向文件系统读取/写入数据,即“输入格式”和“输出格式”。您还需要定义此表如何将数据反序列化为行,或将行序列化为数据,即“serde”。以下选项可用于指定存储格式(“serde”,“输入格式”,“输出格式”),例如CREATE TABLE src(id int) USING hive OPTIONS(fileFormat 'parquet')
。默认情况下,我们将表文件作为纯文本读取。请注意,创建表时尚不支持Hive存储处理程序,您可以使用Hive端的存储处理程序创建表,并使用Spark SQL读取它。
属性名称 | 含义 |
| fileFormat是一种存储格式规范包,包括“serde”,“input format”和“output format”。目前我们支持6种fileFormats:'sequencefile','rcfile','orc','parquet','textfile'和'avro'。 |
| 这两个选项将相应的`InputFormat`和`OutputFormat`类的名称指定为字符串文字,例如`org.apache.hadoop.hive.ql.io.orc.OrcInputFormat`。这两个选项必须出现在pair中,如果已经指定了`fileFormat`选项,则无法指定它们。 |
| 此选项指定serde类的名称。当指定`fileFormat`选项时,如果给定的`fileFormat`已经包含serde的信息,则不要指定此选项。目前“sequencefile”,“textfile”和“rcfile”不包含serde信息,您可以将此选项与这3个fileFormats一起使用。 |
| 这些 |
定义的所有其他属性OPTIONS
将被视为Hive serde属性。
3.5.2.与不同版本的Hive Metastore交互
Spark SQL的Hive支持最重要的部分之一是与Hive Metastore的交互,这使得Spark SQL能够访问Hive表的元数据。从Spark 1.4.0开始,可以使用单个二进制构建的Spark SQL来查询不同版本的Hive Metastores,使用下面描述的配置。请注意,独立于用于与Metastore通信的Hive版本,内部Spark SQL将针对Hive 1.2.1进行编译,并使用这些类进行内部执行(serdes,UDF,UDAF等)。
以下选项可用于配置用于检索元数据的Hive版本:
属性名称 | 默认 | 含义 |
|
| Hive Metastore的版本。可用选项 |
|
| 应该用于实例化HiveMetastoreClient的jar的位置。此属性可以是以下三个选项之一:
|
|
| 以逗号分隔的类前缀列表,应使用在Spark SQL和特定版本的Hive之间共享的类加载器加载。应共享的类的示例是与Metastore进行通信所需的JDBC驱动程序。需要共享的其他类是与已共享的类交互的类。例如,log4j使用的自定义appender。 |
|
| 以逗号分隔的类前缀列表,应为Spark SQL正在与之通信的每个Hive版本显式重新加载。例如,在通常将被共享(即 |
3.6.JDBC到其他数据库
Spark SQL还包括一个可以使用JDBC从其他数据库读取数据的数据源。与使用JdbcRDD相比,此功能应该更受欢迎。这是因为结果作为DataFrame返回,可以在Spark SQL中轻松处理,也可以与其他数据源连接。JDBC数据源也更易于使用Java或Python,因为它不需要用户提供ClassTag。(请注意,这与Spark SQL JDBC服务器不同,后者允许其他应用程序使用Spark SQL运行查询)。
首先,您需要在spark类路径中包含特定数据库的JDBC驱动程序。例如,要从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连接属性。 user
并且password
通常作为用于登录数据源的连接属性提供。除连接属性外,Spark还支持以下不区分大小写的选项:
属性名称 | 含义 |
| 要连接的JDBC URL。可以在URL中指定特定于源的连接属性。例如, |
| 应该读取或写入的JDBC表。请注意,在读取路径中使用它时,可以使用在 |
| 将用于将数据读入Spark的查询。指定的查询将被括起来并在
使用此选项时,以下是一些限制。
|
| 用于连接到此URL的JDBC驱动程序的类名。 |
| 如果指定了任何选项,则必须全部指定这些选项。另外, |
| 表读取和写入中可用于并行的最大分区数。这还确定了最大并发JDBC连接数。如果要写入的分区数超过此限制,我们通过 |
| 驱动程序等待Statement对象执行到指定秒数的秒数。零意味着没有限制。在写入路径中,此选项取决于JDBC驱动程序如何实现API |
| JDBC提取大小,用于确定每次往返要获取的行数。这可以帮助JDBC驱动程序的性能,默认为低读取大小(例如,Oracle有10行)。此选项仅适用于阅读。 |
| JDBC批处理大小,用于确定每次往返要插入的行数。这可以帮助JDBC驱动程序的性能。此选项仅适用于书写。它默认为 |
| 事务隔离级别,适用于当前连接。它可以是一个 |
| 在向远程数据库打开每个数据库会话之后,在开始读取数据之前,此选项将执行自定义SQL语句(或PL / SQL块)。使用它来实现会话初始化代码。例: |
| 这是JDBC编写器相关选项。当 |
| 这是JDBC编写器相关选项。如果JDBC数据库(PostgreSQL和Oracle目前)启用并支持,则此选项允许执行a |
| 这是JDBC编写器相关选项。如果指定,则此选项允许在创建表时设置特定于数据库的表和分区选项(例如, |
| 创建表时要使用的数据库列数据类型而不是默认值。数据类型信息应以与CREATE TABLE列语法相同的格式指定(例如: |
| 用于从JDBC连接器读取数据的自定义架构。例如, |
| 用于启用或禁用谓词下推到JDBC数据源的选项。默认值为true,在这种情况下,Spark会尽可能地将过滤器下推到JDBC数据源。否则,如果设置为false,则不会将过滤器下推到JDBC数据源,因此所有过滤器都将由Spark处理。当Spark通过比JDBC数据源更快地执行谓词过滤时,谓词下推通常会被关闭。 |
// Note: JDBC loading and saving can be achieved via either the load/save or jdbc methods
// Loading data from a JDBC source
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);
// 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);
3.7.Apache Avro 数据源指南
自Spark 2.4发布以来,Spark SQL为读取和编写Apache Avro数据提供了内置支持。
3.7.1.部署
该spark-avro
模块是外部的,不包括在内spark-submit
或spark-shell
默认情况下。
与任何Spark应用程序一样,spark-submit
用于启动您的应用程序。spark-avro_2.12
并且它的依赖关系可以直接添加到spark-submit
使用--packages
,例如,
./bin/spark-submit --packages org.apache.spark:spark-avro_2.12:2.4.3 ...
对于实验spark-shell
,您还可以使用直接--packages
添加org.apache.spark:spark-avro_2.12
及其依赖项,
./bin/spark-shell --packages org.apache.spark:spark-avro_2.12:2.4.3 ...
有关提交具有外部依赖性的应用程序的详细信息,请参阅“ 应用程序提交指南
3.7.2.加载和保存功能
由于spark-avro
模块是外部的,因此没有.avro
API DataFrameReader
或DataFrameWriter
。
要以Avro格式加载/保存数据,您需要将数据源选项指定format
为avro
(或org.apache.spark.sql.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");
3.7.3.to_avro() and from_avro()
Avro软件包提供to_avro
了将列编码为Avro格式的二进制文件,以及from_avro()
将Avro二进制数据解码为列的功能。两个函数都将一列转换为另一列,输入/输出SQL数据类型可以是复杂类型或基本类型。
在读取或写入像Kafka这样的流媒体源时,将Avro记录用作列非常有用。每个Kafka键值记录都会增加一些元数据,例如Kafka的摄取时间戳,Kafka的偏移量等。
- 如果包含您的数据的“值”字段位于Avro中,您可以使用它
from_avro()
来提取数据,丰富数据,清理数据,然后再将其下游推送到Kafka或将其写入文件。 -
to_avro()
可用于将结构转换为Avro记录。在将数据写入Kafka时,如果要将多个列重新编码为单个列,此方法特别有用。
import org.apache.spark.sql.avro.*;
// `from_avro` requires Avro schema in JSON string format.
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. Decode the Avro data into a struct;
// 2. Filter by column `favorite_color`;
// 3. Encode the column `name` in Avro format.
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();
3.7.4.数据源选项
可以使用或.option
上的方法设置Avro的数据源选项。DataFrameReader
DataFrameWriter
属性名称 | 默认 | 含义 | 范围 |
| 没有 | 用户以JSON格式提供的可选Avro架构。记录字段的日期类型和命名应与输入的Avro数据或Catalyst数据匹配,否则读/写操作将失败。 | 读和写 |
| topLevelRecord | 写入结果中的顶级记录名称,在Avro规范中是必需的。 | 写 |
| “” | 在写入结果中记录名称空间 | 写 |
| 真正 | 该选项控制忽略没有 如果启用该选项, | 读 |
| 瞬间 | 该 目前支持的编解码器 如果未设置该选项, | 写 |
3.7.5.组态
可以使用setConf
SparkSession上的方法或SET key=value
使用SQL 运行命令来完成Avro的配置
属性名称 | 默认 | 含义 |
spark.sql.legacy.replaceDatabricksSparkAvro.enabled | 真正 | 如果设置为true,则数据源提供程序 |
spark.sql.avro.compression.codec | 瞬间 | 用于编写AVRO文件的压缩编解码器。支持的编解码器:uncompressed,deflate,snappy,bzip2和xz。默认编解码器很快。 |
spark.sql.avro.deflate.level | -1 | 用于编写AVRO文件的deflate编解码器的压缩级别。有效值必须介于1到9之间(包括1或9)或-1。默认值为-1,对应于当前实现中的6级。 |
3.7.6.与Databricks spark-avro的兼容性
此Avro数据源模块最初来自Databricks的开源存储库spark-avro并与之兼容 。
默认情况下,spark.sql.legacy.replaceDatabricksSparkAvro.enabled
启用SQL配置后,数据源提供程序com.databricks.spark.avro
将映射到此内置Avro模块。对于使用目录元存储中的Provider
属性创建的Spark表com.databricks.spark.avro
,如果使用此内置Avro模块,则映射对于加载这些表至关重要。
请注意Databricks的spark-avro,隐式类 AvroDataFrameWriter
,AvroDataFrameReader
并为快捷功能创建.avro()
。在这个内置但外部的模块中,两个隐式类都被删除了。请使用.format("avro")
在 DataFrameWriter
或DataFrameReader
代替,这应该是足够的清洁和良好的。
如果您更喜欢使用自己的spark-avro
jar文件构建,则可以简单地禁用配置spark.sql.legacy.replaceDatabricksSparkAvro.enabled
,并--jars
在部署应用程序时使用该选项。有关详细信息,请阅读“ 应用程序提交指南”中的“ 高级依赖关系管理”部分
3.7.7.支持的Avro类型 - > Spark SQL转换
目前,Spark支持在Avro记录下读取所有原始类型和复杂类型。
Avro type | Spark SQL type |
boolean | BooleanType |
int | IntegerType |
long | LongType |
float | FloatType |
double | DoubleType |
string | StringType |
enum | StringType |
fixed | BinaryType |
bytes | BinaryType |
record | StructType |
array | ArrayType |
map | MapType |
union | See below |
除了上面列出的类型,它还支持阅读union
类型。以下三种类型被认为是基本union
类型:
-
union(int, long)
将映射到LongType。 -
union(float, double)
将映射到DoubleType。 -
union(something, null)
,任何支持Avro类型的东西。这将被映射到与某事物相同的Spark SQL类型,并将nullable设置为true。所有其他联合类型都被认为是复杂的 根据union的成员,它们将映射到StructType,其中字段名称是member0,member1等。这与Avro和Parquet之间的转换行为一致。
它还支持读取以下Avro 逻辑类型:
Avro logical type | Avro type | Spark SQL type |
date | int | DateType |
timestamp-millis | long | TimestampType |
timestamp-micros | long | TimestampType |
decimal | fixed | DecimalType |
decimal | bytes | DecimalType |
3.7.8.支持的Spark SQL类型 - > Avro转换
Spark支持将所有Spark SQL类型写入Avro。对于大多数类型,从Spark类型到Avro类型的映射很简单(例如,IntegerType转换为int); 但是,下面列出了一些特殊情况:
Spark SQL type | Avro type | Avro logical type |
ByteType | int | |
ShortType | int | |
BinaryType | bytes | |
DateType | int | date |
TimestampType | long | timestamp-micros |
DecimalType | fixed | decimal |
您还可以使用该选项指定整个输出Avro架构avroSchema
,以便可以将Spark SQL类型转换为其他Avro类型。默认情况下不应用以下转换,并且需要用户指定的Avro架构:
Spark SQL type | Avro type | Avro logical type |
BinaryType | fixed | |
StringType | enum | |
TimestampType | long | timestamp-millis |
DecimalType | bytes | decimal |
3.8.故障排除
- 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日期被解析为时间戳。
4.性能调优
对于某些工作负载,可以通过在内存中缓存数据或打开一些实验选项来提高性能。
4.1.在内存中缓存数据
Spark SQL可以通过调用spark.catalog.cacheTable("tableName")
或使用内存中的列式格式来缓存表dataFrame.cache()
。然后,Spark SQL将仅扫描所需的列,并自动调整压缩以最小化内存使用和GC压力。您可以调用spark.catalog.uncacheTable("tableName")
从内存中删除表。
可以使用SQL setConf
上的方法SparkSession
或通过SET key=value
使用SQL 运行 命令来完成内存中缓存的配置。
Property Name | Default | Meaning |
| true | 设置为true时,Spark SQL将根据数据统计信息自动为每列选择压缩编解码器。 |
| 10000 | 控制柱状缓存的批次大小。较大的批处理大小可以提高内存利用率和压缩率,但在缓存数据时存在OOM风险。 |
4.2.其他配置选项
以下选项也可用于调整查询执行的性能。由于更多优化会自动执行,因此在将来的版本中可能会弃用这些选项。
属性名称 | Default | Meaning |
| 134217728 (128 MB) | The maximum number of bytes to pack into a single partition when reading files. |
| 4194304 (4 MB) | The estimated cost to open a file, measured by the number of bytes could be scanned in the same time. This is used when putting multiple files into a partition. It is better to over-estimated, then the partitions with small files will be faster than partitions with bigger files (which is scheduled first). |
| 300 | Timeout in seconds for the broadcast wait time in broadcast joins |
| 10485760 (10 MB) | Configures the maximum size in bytes for a table that will be broadcast to all worker nodes when performing a join. By setting this value to -1 broadcasting can be disabled. Note that currently statistics are only supported for Hive Metastore tables where the command |
| 200 | Configures the number of partitions to use when shuffling data for joins or aggregations. |
4.3.SQL查询的广播提示
该BROADCAST
提示引导件Spark与另一个表或视图接合它们时广播的每个指定的表。当Spark决定连接方法时,广播散列连接(即BHJ)是首选,即使统计数据高于配置spark.sql.autoBroadcastJoinThreshold
。指定连接的两端时,Spark会广播具有较低统计信息的那一方。注意Spark并不保证始终选择BHJ,因为并非所有情况(例如全外连接)都支持BHJ。当选择广播嵌套循环连接时,我们仍然尊重提示。
import static org.apache.spark.sql.functions.broadcast;
broadcast(spark.table("src")).join(spark.table("records"), "key").show();
5.分布式SQL引擎
Spark SQL还可以使用其JDBC / ODBC或命令行界面充当分布式查询引擎。在此模式下,最终用户或应用程序可以直接与Spark SQL交互以运行SQL查询,而无需编写任何代码
5.1.运行Thrift JDBC / ODBC服务器
此处实现的Thrift JDBC / ODBC服务器对应HiveServer2 于Hive 1.2.1中的。您可以使用Spark或Hive 1.2.1附带的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
使用以下方式直接连接到JDBC / ODBC服务器:
beeline> !connect jdbc:hive2://localhost:10000
直线会询问您的用户名和密码。在非安全模式下,只需在您的计算机上输入用户名和空白密码即可。对于安全模式,请按照直线文档中的 说明进行操作。
Hive的结构是通过将您做hive-site.xml
,core-site.xml
和hdfs-site.xml
文件conf/
您也可以使用Hive附带的beeline脚本。
Thrift JDBC服务器还支持通过HTTP传输发送thrift RPC消息。使用以下设置将HTTP模式作为系统属性启用或在hive-site.xml
文件中启用conf/
:
hive.server2.transport.mode - Set this to value: http
hive.server2.thrift.http.port - HTTP port number to listen on; default is 10001
hive.server2.http.endpoint - HTTP endpoint; default is cliservice
要进行测试,请使用beeline以http模式连接到JDBC / ODBC服务器:
beeline> !connect jdbc:hive2://<host>:<port>/<database>?hive.server2.transport.mode=http;hive.server2.thrift.http.path=<http_endpoint>
5.2.运行Spark SQL CLI
Spark SQL CLI是一种方便的工具,可以在本地模式下运行Hive Metastore服务,并执行从命令行输入的查询。请注意,Spark SQL CLI无法与Thrift JDBC服务器通信。
要启动Spark SQL CLI,请在Spark目录中运行以下命令:
./bin/spark-sql
Hive的结构是通过将您做hive-site.xml
,core-site.xml
和hdfs-site.xml
文件conf/
。您可以运行./bin/spark-sql --help
所有可用选项的完整列表。
6.使用Apache Arrow的Pandas PySpark使用指南
6.1.Spark中的Apache Arrow
Apache Arrow是一种内存中的列式数据格式,在Spark中用于在JVM和Python进程之间有效地传输数据。这对于使用Pandas / NumPy数据的Python用户来说是最有益的。它的使用不是自动的,可能需要对配置或代码进行一些小的更改才能充分利用并确保兼容性。本指南将提供有关如何在Spark中使用Arrow的高级描述,并在使用启用箭头的数据时突出显示任何差异。
6.1.1.确保PyArrow已安装
如果使用pip安装PySpark,则可以使用该命令将PyArrow作为SQL模块的额外依赖项引入pip install pyspark[sql]
。否则,您必须确保在所有群集节点上安装并可用PyArrow。当前支持的版本是0.8.0。您可以使用conda-forge通道中的pip或conda进行安装。有关详细信息,请参阅PyArrow 安装。
6.2.启用与Pandas的转换
使用调用将Spark DataFrame转换为Pandas DataFrame toPandas()
时以及使用Pandas DataFrame 创建Spark DataFrame时,Arrow可用作优化createDataFrame(pandas_df)
。要在执行这些调用时使用Arrow,用户需要先将Spark配置'spark.sql.execution.arrow.enabled'设置为'true'。默认情况下禁用此功能。
此外,如果在Spark中的实际计算之前发生错误,则由'spark.sql.execution.arrow.enabled'启用的优化可以自动回退到非Arrow优化实现。这可以通过'spark.sql.execution.arrow.fallback.enabled'来控制。
# Python
import numpy as np
import pandas as pd
# Enable Arrow-based columnar data transfers
spark.conf.set("spark.sql.execution.arrow.enabled", "true")
# Generate a Pandas DataFrame
pdf = pd.DataFrame(np.random.rand(100, 3))
# Create a Spark DataFrame from a Pandas DataFrame using Arrow
df = spark.createDataFrame(pdf)
# Convert the Spark DataFrame back to a Pandas DataFrame using Arrow
result_pdf = df.select("*").toPandas()
"examples/src/main/python/sql/arrow.py"
使用上述箭头优化将产生与未启用箭头时相同的结果。请注意,即使使用Arrow,也会toPandas()
导致将DataFrame中的所有记录收集到驱动程序中,并且应该在一小部分数据上完成。当前不支持所有Spark数据类型,如果列具有不受支持的类型,则可能引发错误,请参阅支持的SQL类型。如果在此期间发生错误createDataFrame()
,Spark将回退以创建没有Arrow的DataFrame。
6.2.Pandas UDF(又名矢量化UDF)
Pandas UDF是用户定义的函数,由Spark使用Arrow执行传输数据和Pandas以处理数据。使用关键字pandas_udf
作为装饰器或包装函数来定义Pandas UDF ,不需要其他配置。目前,有两种类型的Pandas UDF:Scalar和Grouped Map。
6.2.1.Scalar
Scalar Pandas UDF用于矢量化标量操作。它们可与功能,如可以使用select
和withColumn
。Python函数应该pandas.Series
作为输入并返回pandas.Series
相同长度的。在内部,Spark将执行Pandas UDF,方法是将列拆分为批次,并将每个批次的函数作为数据的子集调用,然后将结果连接在一起。
以下示例显示如何创建计算2列乘积的Scalar Pandas UDF。
# Python
import pandas as pd
from pyspark.sql.functions import col, pandas_udf
from pyspark.sql.types import LongType
# Declare the function and create the UDF
def multiply_func(a, b):
return a * b
multiply = pandas_udf(multiply_func, returnType=LongType())
# The function for a pandas_udf should be able to execute with local Pandas data
x = pd.Series([1, 2, 3])
print(multiply_func(x, x))
# 0 1
# 1 4
# 2 9
# dtype: int64
# Create a Spark DataFrame, 'spark' is an existing SparkSession
df = spark.createDataFrame(pd.DataFrame(x, columns=["x"]))
# Execute function as a Spark vectorized UDF
df.select(multiply(col("x"), col("x"))).show()
# +-------------------+
# |multiply_func(x, x)|
# +-------------------+
# | 1|
# | 4|
# | 9|
# +-------------------+
"examples/src/main/python/sql/arrow.py"
6.2.2.Grouped Map
使用分组映射Pandas UDF groupBy().apply()
实现“split-apply-combine”模式。Split-apply-combine包含三个步骤:
- 使用将数据拆分为组
DataFrame.groupBy
。 - 在每个组上应用一个功能。函数的输入和输出都是
pandas.DataFrame
。输入数据包含每个组的所有行和列。 - 将结果合并为一个新的
DataFrame
。
要使用groupBy().apply()
,用户需要定义以下内容:
- 一个Python函数,用于定义每个组的计算。
- 甲
StructType
对象或定义输出的模式的字符串DataFrame
。
pandas.DataFrame
如果指定为字符串,则返回的列标签必须与定义的输出模式中的字段名称匹配,或者如果不是字符串,则匹配字段数据类型,例如整数索引。请参阅pandas.DataFrame ,了解如何在构造时标记列pandas.DataFrame
。
请注意,在应用函数之前,组的所有数据都将加载到内存中。这可能导致内存不足异常,尤其是在组大小偏斜的情况下。maxRecordsPerBatch的配置 不适用于组,并且由用户决定分组数据是否适合可用内存。
以下示例显示如何使用groupby().apply()
从组中的每个值中减去平均值。
from pyspark.sql.functions import pandas_udf, PandasUDFType
df = spark.createDataFrame(
[(1, 1.0), (1, 2.0), (2, 3.0), (2, 5.0), (2, 10.0)],
("id", "v"))
@pandas_udf("id long, v double", PandasUDFType.GROUPED_MAP)
def subtract_mean(pdf):
# pdf is a pandas.DataFrame
v = pdf.v
return pdf.assign(v=v - v.mean())
df.groupby("id").apply(subtract_mean).show()
# +---+----+
# | id| v|
# +---+----+
# | 1|-0.5|
# | 1| 0.5|
# | 2|-3.0|
# | 2|-1.0|
# | 2| 4.0|
# +---+----+
有关详细用法,请参阅pyspark.sql.functions.pandas_udf和 pyspark.sql.GroupedData.apply。
6.2.3.分组聚合(Grouped Aggregate)
分组聚合Pandas UDF类似于Spark聚合函数。分组聚合Pandas UDF与groupBy().agg()
和 一起使用pyspark.sql.Window。它定义从一个或多个pandas.Series
到标量值的聚合,其中每个聚合pandas.Series
表示组或窗口中的列。
请注意,此类型的UDF不支持部分聚合,组或窗口的所有数据都将加载到内存中。此外,目前只有Grouped聚合Pandas UDF支持无界窗口。
以下示例显示如何使用此类型的UDF来计算groupBy和窗口操作的平均值:
from pyspark.sql.functions import pandas_udf, PandasUDFType
from pyspark.sql import Window
df = spark.createDataFrame(
[(1, 1.0), (1, 2.0), (2, 3.0), (2, 5.0), (2, 10.0)],
("id", "v"))
@pandas_udf("double", PandasUDFType.GROUPED_AGG)
def mean_udf(v):
return v.mean()
df.groupby("id").agg(mean_udf(df['v'])).show()
# +---+-----------+
# | id|mean_udf(v)|
# +---+-----------+
# | 1| 1.5|
# | 2| 6.0|
# +---+-----------+
w = Window \
.partitionBy('id') \
.rowsBetween(Window.unboundedPreceding, Window.unboundedFollowing)
df.withColumn('mean_v', mean_udf(df['v']).over(w)).show()
# +---+----+------+
# | id| v|mean_v|
# +---+----+------+
# | 1| 1.0| 1.5|
# | 1| 2.0| 1.5|
# | 2| 3.0| 6.0|
# | 2| 5.0| 6.0|
# | 2|10.0| 6.0|
# +---+----+------+
有关详细用法,请参阅 pyspark.sql.functions.pandas_udf
6.3.使用说明
6.3.1支持的SQL类型
目前,所有Spark SQL数据类型是基于箭转换,除了支持MapType
, ArrayType
中TimestampType
和嵌套StructType
。BinaryType
仅在安装PyArrow等于或高于0.10.0时才支持。
6.3.2设置箭头批量大小
Spark中的数据分区将转换为箭头记录批次,这可能会暂时导致JVM中的高内存使用量。为避免可能的内存不足异常,可以通过设置“spark.sql.execution.arrow.maxRecordsPerBatch”设置为一个整数来调整箭头记录批次的大小,该整数将确定每个批次的最大行数。默认值为每批10,000条记录。如果列数很大,则应相应地调整该值。使用此限制,每个数据分区将被制成一个或多个记录批次以进行处理。
6.3.3.带时区语义的时间戳
Spark在内部将时间戳存储为UTC值,并且在没有指定时区的情况下引入的时间戳数据将以本地时间转换为UTC,并具有微秒分辨率。在Spark中导出或显示时间戳数据时,会话时区用于本地化时间戳值。会话时区使用配置'spark.sql.session.timeZone'设置,如果未设置,将默认为JVM系统本地时区。Pandas使用datetime64
具有纳秒分辨率的类型datetime64[ns]
,并且每列具有可选的时区。
当时间戳数据从Spark传输到Pandas时,它将转换为纳秒,每列将转换为Spark会话时区,然后本地化到该时区,这将删除时区并将值显示为本地时间。调用toPandas()
或pandas_udf
使用timestamp列时会发生这种情况。
当时间戳数据从Pandas传输到Spark时,它将转换为UTC微秒。createDataFrame
使用Pandas DataFrame 调用或从a返回时间戳时 会发生这种情况pandas_udf
。这些转换是自动完成的,以确保Spark具有预期格式的数据,因此不必自己进行任何这些转换。任何纳秒值都将被截断。
请注意,标准UDF(非Pandas)会将时间戳数据作为Python日期时间对象加载,这与Pandas时间戳不同。在pandas_udf
s中使用时间戳时,建议使用Pandas时间序列功能以获得最佳性能