Spark SQL支持通过DataFrame接口对各种数据源进行操作。DataFrame可以使用关系转换进行操作,也可以用于创建临时视图。将DataFrame注册为临时视图允许您对其数据运行SQL查询。
通用加载/保存功能
在最简单的形式中,默认数据源(parquet除非另有配置spark.sql.sourcess.default)将用于所有操作。
Dataset<Row> usersDF = spark.read().load("examples/src/main/resources/users.parquet");
usersDF.select("name", "favorite_color").write().save("namesAndFavColors.parquet");
手动指定选项
我们还可以手动指定将要使用的数据源以及要传递给数据源的任何其他选项。数据源通过其全名指定(即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");
直接在文件上运行SQL
我们可以直接使用SQL查询该文件,而不是使用读取API将文件加载到DataFrame并进行查询。
Dataset<Row> sqlDF =
spark.sql("SELECT * FROM parquet.`examples/src/main/resources/users.parquet`");
保存模式
保存操作可以选择使用一个SaveMode,指定如何处理现有数据(如果存在)。重要的是要意识到这些保存模式不适用任何锁定并且不是原子的。此外,执行Overwrite,数据将在写出新数据之前被删除。
Scala/Java | Any Language | Meaning |
SaveMode.ErrorIfExists (default) | "error" or "errorifexists" (default) | 将DataFrame保存到数据源时,如果数据已存在,则会引发异常 |
SaveMode.Append | "append" | 将DataFrame保存到数据源时,如果数据表已存在,则DataFrame的内容应附加到现有数据。 |
SaveMode.Overwrite | "overwrite" | 覆盖模式意味着在将DataFrame保存到数据源时,如果数据表已经存在,则预期现有数据将被DataFrame的r |
SaveMode.Ignore | "ignore" | 忽略模式意味着在将DataFrame保存到数据源时,如果数据已存在,则预期保存操作不会保存DataFrame的内容并且不会更改现有数据。这与SQL中的CREATE TABLE IF NOT EXISTS相似。 |
保存到持久表
DataFrame也可以使用saveAsTable命令将表持久化到Hive Metastore中。请注意,使用此功能不需要现有的Hive部署。Spark将为我们出啊关键默认的本地Hive Metastore(使用Derby)。与createOrReplaceTempView命令不同,saveAsTable将实现DataFrame的内容并创建指向Hive Metastore中数据的指针。只要我们保持与同一个Metastore的连接,即使我们的Spark重启后,持久表依然存在。可以通过使用表的名称调用table的一个SparkSession上的方法来创建持久表的DataFrame。
对于基于文件的数据源,例如text,parquet,json等,我们可以通过path选项指定自定义表路径,例如df.write.option("path", "/some/path").saveAsTable("t");。删除表时,将不会删除自定义表路径,并且表数据仍然存在。如果未指定自定义表路径,则Spark会将数据写入仓库目录下的默认表路径。删除表时,也将删除默认表路径。
从Spark2.1开始,持久数据源表将每个分区元数据存储在Hive Metastore中。这带来了几个好处:
- 由于Metastore只能返回查询所需要的分区,因此不再需要在表的第一个查询中发现所有分区。
- Hive DDL ALTER TABLE PARTITION ... SET LOCATION现在可以用于使用Datasource API创建的表。
请注意在创建外部数据源表(带有path选项的表)时,默认情况下不会收集分区信息。要同步Metastore中的分区信息,可以调用MSCK REPAIR TABLE。
桶,排序和分区
对于基于文件的数据源,还可以对输出进行存储和排序或分区。分段和排序仅适用于持久表:
peopleDF.write().bucketBy(42, "name").sortBy("age").saveAsTable("people_bucketed");
当使用Dataset API进行分区操作时,可以同时使用save和saveAsTable
usersDF
.write()
.partitionBy("favorite_color")
.format("parquet")
.save("namesPartByColor.parquet");
可以对单个表使用分区和分桶:
peopleDF
.write()
.partitionBy("favorite_color")
.bucketBy(42, "name")
.saveAsTable("people_partitioned_bucketed");
partitionBy创建目录结构。他对具有高基数的列的实用性有限。相反bucketBy,在固定数量的桶中分配数据,并且可以在许多唯一值无限制时使用。
Parquet文件
Parquet时一种柱状格式,许多其他数据处理系统都支持它。Spark SQL支持读取和写入Parquet文件,这些文件自动保留原始数据的模式。在编写Parquet文件时,出于兼容性原因,所有列都会自动转换为可为空。
以编程方式加载数据
使用上面示例中的数据:
private static void runBasicParquetExample(SparkSession spark) {
// $example on:basic_parquet_example$
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|
// +------------+
// $example off:basic_parquet_example$
}
分区发现
表分区是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。禁用类型推断时,字符串类型将用于分区列。如果用户需要指定分区发现应该开始的基本路径,则可以在数据源选项中对basePath进行设置。例如,当数据的路径为path/to/table/gender=male并且用户设置的basePath为path/to/table,那么gender将是一个分区列。
架构合并
与ProtocolBuffer,Avro和Thrift一样,Parquet也支持模式演变。用户可以从简单模式开始,并根据需要主键向模式添加更多列。通过这种方式,用户可能最终得到具有不同但相互兼容的模式的多个Parquet文件。Parquet数据源现在能够自动检测这种情况并合并所有文件的模式。
由于模式合并是一项相对昂贵的操作,并且在大多数情况下不是必须的,因为我们默认从1.5.0开始关闭它,我们可以启用它:
- 数据源选项设置mergeSchema到true读取Parquet文件
- 将全局SQL选项设置spark.sql.parquet.mergeSchema为true。
public static class Square implements Serializable {
private int value;
private int square;
// Getters and setters...
// $example off:schema_merging$
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public int getSquare() {
return square;
}
public void setSquare(int square) {
this.square = square;
}
// $example on:schema_merging$
}
// $example on:schema_merging$
public static class Cube implements Serializable {
private int value;
private int cube;
// Getters and setters...
// $example off:schema_merging$
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public int getCube() {
return cube;
}
public void setCube(int cube) {
this.cube = cube;
}
// $example on:schema_merging$
}
private static void runParquetSchemaMergingExample(SparkSession spark) {
// $example on:schema_merging$
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)
// $example off:schema_merging$
}
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 MetaStroe Parquet表转换为Spark SQL Parquet表时,我们必须将Hive Metastore模式与Parquet模式进行协调。规则是:
- 两个模式中具有相同名称的字段必须具有相同的数据类型,而不管是否为空。协调字段应具有Parquet端的数据类型,以便遵循可为空性。
- 协调的模式(schema)恰好包含Hive Metastroe模式中定义的那些字段
- 仅出现在Parquet模式中的任何字段都将放入已协调的模式中。
- 仅出现在Hive Metastore模式中的任何字段都将在协调模式中添加为可空字段。
元数据刷新
Spark SQL缓存Parquet元数据以获得更好的性能。启用Hive Metastore Parquet表转换后,还会缓存这些转换表的元数据。如果这些表由Hive或其他外部工具更新,则需要手动刷新它们以确保元数据一致。
// spark is an existing SparkSession
spark.catalog().refreshTable("my_table");
组态
可以使用SQL SparkSession上的方法setConf或通过SET key=value使用SQL运行命令来完成Parquet的配置。
属性名 | 缺省值 | 含义 |
spark.sql.parquet.binaryAsString | false | 其他一些Paruqet生成系统,特别是Impala,Hive和旧版本的Spark SQL,在写出Parquet模式时不区分二进制数据和字符串。此表示告诉Spark SQL将二进制数据解释为字符串,以提供与这些系统的兼容性 |
spark.sql.parquet.int96AsTimestamp | true | 一些Parquet生产系统,特别是Impala和Hive,将时间戳存储到INT96中。此标志告诉Spark SQL将INT96数据解释为时间戳,以提供与这些系统的兼容性 |
spark.sql.parquet.compression.codec | snappy | 设置编写Parquet文件时使用的压缩编解码器。如果在特定于表的选项/属性中指定了“compression”,“parquet.compression”,“spark.sql.parquet.compression.codec”。可接受的值包括:none,uncompressed,snappy,gzip,lzo。 |
spark.sql.parquet.filterPushdown | true | 设置为true时启用Parquet过滤器下推优化。 |
spark.sql.hive.convertMetastoreParquet | true | 设置为false时,Spark SQL将使用Hive SerDe作为Parquet表而不是内置支持 |
spark.sql.parquet.mergeSchema | false | 如果为true,则Parquet数据源合并从所有数据文件收集的schema,否则,如果没有可用的摘要文件,则从摘要文件或随机数据文件中选取模式 |
spark.sql.optimizer.metadataOnly | true | 如果为true,则启用使用表的元数据的仅元数据查询优化,以生成分区列而不是表扫描。当扫描的所有列都是分区列并且查询具有满足不同语义的聚合运算符时,它使用。 |