SparkSQL編程指南之Java篇二-數據源(上)

Spark SQL通過DataFrame接口支持各種不同數據源的操作。一個DataFrame可以進行相關的轉換操作,也可以用於創建臨時視圖。註冊DataFrame爲一個臨時視圖可以允許你對其數據執行SQL查詢。本文首先會介紹使用Spark數據源加載和保存數據的一般方法,然後對內置數據源進行詳細介紹。

1. 一般的Load/Save方法

Spark SQL最簡單的也是默認的數據源格式是Parquet(除非使用了spark.sql.sources.default配置修改),它將會被用於所有的操作。以下是一般的Load/Save方法:

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

* 如果是使用windows系統的話,需要把對應版本編譯的hadoop.dll複製到C:\Windows\System32,否則會遇到以下錯誤:

java.lang.UnsatisfiedLinkError: org.apache.hadoop.io.nativeio.NativeIO$Windows.createFileWithMode0(Ljava/lang/String;JJJI)Ljava/io/FileDescriptor;
	at org.apache.hadoop.io.nativeio.NativeIO$Windows.createFileWithMode0(Native Method)
	at org.apache.hadoop.io.nativeio.NativeIO$Windows.createFileOutputStreamWithMode(NativeIO.java:559)
	at org.apache.hadoop.fs.RawLocalFileSystem$LocalFSFileOutputStream.<init>(RawLocalFileSystem.java:219)
	at org.apache.hadoop.fs.RawLocalFileSystem$LocalFSFileOutputStream.<init>(RawLocalFileSystem.java:209)
	at org.apache.hadoop.fs.RawLocalFileSystem.createOutputStreamWithMode(RawLocalFileSystem.java:307)
	at org.apache.hadoop.fs.RawLocalFileSystem.create(RawLocalFileSystem.java:296)
	at org.apache.hadoop.fs.RawLocalFileSystem.create(RawLocalFileSystem.java:328)
	at org.apache.hadoop.fs.ChecksumFileSystem$ChecksumFSOutputSummer.<init>(ChecksumFileSystem.java:398)
	at org.apache.hadoop.fs.ChecksumFileSystem.create(ChecksumFileSystem.java:461)
	at org.apache.hadoop.fs.ChecksumFileSystem.create(ChecksumFileSystem.java:440)
	at org.apache.hadoop.fs.FileSystem.create(FileSystem.java:911)
	at org.apache.hadoop.fs.FileSystem.create(FileSystem.java:892)
	at org.apache.hadoop.fs.FileSystem.create(FileSystem.java:789)
	at org.apache.parquet.hadoop.ParquetFileWriter.<init>(ParquetFileWriter.java:223)
    ......

1.1 手動指定選項

我們也可以通過完整的全名(例如:org.apache.spark.sql.parquet)來指定數據源的類型,對於那些內置的數據源類型,也可以使用簡稱,例如:json, parquet, jdbc, orc, libsvm, csv, text。從任何數據源類型加載的DataFrames可以轉換爲其它的類型格式,例如:

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

1.2 直接執行SQL

我們也可以直接執行SQL查詢而不需要使用API加載文件爲DataFrame然後再查詢,例如:

// run SQL on files directly
Dataset<Row> sqlDF = spark.sql("SELECT * FROM parquet.`examples/src/main/resources/users.parquet`");
sqlDF.show();
// +------+--------------+----------------+
// |  name|favorite_color|favorite_numbers|
// +------+--------------+----------------+
// |Alyssa|          null|  [3, 9, 15, 20]|
// |   Ben|           red|              []|
// +------+--------------+----------------+

* 注意parquet.後面的路徑前後有個`的字符(與鍵盤~一起的那個逗點)

1.3 保存模式

保存操作可以選擇性地使用SaveMode指定如何處理存在的數據。需要注意的是這些保存模式不使用任何鎖和不是原子操作的。此外,當使用Overwrite模式時,原數據會在寫入新數據之前就會被刪除。以下是SaveMode的選項,當保存一個DataFrame到指定數據源的時候,如果輸出路徑已經存在:

SaveMode.ErrorIfExists(默認)     拋出異常
SaveMode.Append                      數據會以追加的方式保存
SaveMode.Overwrite                   新數據會覆蓋原數據(先刪除原數據,再保存新數據)
SaveMode.Ignore                        不保存新數據,相當於SQL語句的CREATE TABLE IF NOT EXISTS

例如:

usersDF.select("name", "favorite_color").write().mode(SaveMode.Overwrite).save("namesAndFavColors.parquet");

1.4 持久化到表

我們也可以使用saveAsTable方法把DataFrames保存爲持久化的表到Hive metastore。值得注意的是我們不需要部署Hive的環境,Spark會創建一個默認本地的Hive metastore(使用Derby)。與createOrReplaceTempView方法不同,saveAsTable會實質化DataFrame的內容,然後在Hive metastore創建它的指針。只要連接到相同metastore的連接不中斷,即使Spark程序重新啓動,持久化的表也會一直存在。

默認地saveAsTable方法將創建一個“管理表”(managed table),表示數據的位置是由metastore來控制管理的。當持久化的表被刪除時,managed table將會自動刪除相應的數據。

2. Parquet文件

Parquet是一種被其它多種數據處理系統支持的縱列格式。Spark SQL提供了讀寫Parquet文件的功能,保存的Parquet文件會自動保留原始數據的schema。當保存Parquet文件時,基於兼容性考慮,所有的列會被自動轉換爲允許空值。

2.1 以編程方式加載數據

import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
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(row -> "Name: " + row.getString(0), Encoders.STRING());
namesDS.show();
// +------------+
// |       value|
// +------------+
// |Name: Justin|
// +------------+

2.2 分區推斷

對錶進行分區是對數據進行優化的方式之一。在一個分區的表內,數據通常是通過分區列將數據存儲在不同的目錄裏面。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的Schema如下:

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

需要注意的是,分區列的數據類型是自動解析的。目前,數值類型和字符串類型是支持的。如果不想分區列的數據類型被自動解析,可以通過配置spark.sql.sources.partitionColumnTypeInference.enabled=false,默認是true。當該配置被設爲false時,分區列數據類型會使用string類型。

從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將會是分區列。

2.3 Schema合併

像ProtocolBuffer、Avro和Thrift一樣,Parquet也支持schema evolution(schema演變)。用戶可以先定義一個簡單的schema,然後根據需要逐漸地向schema中增加列。通過這種方式,用戶可以有多個不同的schemas但它們是互相兼容的Parquet文件。Parquet數據源現在能夠自動檢測這種情況併合並這些文件的schemas。

因爲Schema合併是一個相對高消耗的操作,在大多數的情況下並不需要,所以從Spark SQL 1.5.0版本開始,默認關閉了該功能。可以通過下面兩種方式開啓:
  • 當讀取Parquet文件時,設置數據源選項mergeSchema=true(例如下面的例子)
  • 設置全局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)

2.4 Hive metastore Parquet錶轉換

當讀寫Hive metastore Parquet表時,基於性能考慮,Spark SQL會先嚐試使用自帶的Parquet SerDe(序列化與反序列化,Serialize/Deserilize的簡稱),而不是Hive的SerDe。這個優化選項可以通過spark.sql.hive.convertMetastoreParquet配置,默認爲開啓。

2.4.1 Hive/Parquet Schema一致化

從表schema處理的角度來看,Hive和Parquet有2個主要的不同點:
  • Hive不區分大小寫,Parquet區分大小寫
  • Hive認爲所有的列都可以爲空,而Parquet的空值性是有重要意義的(Hive considers all columns nullable, while nullability in Parquet is significant)
由於以上不同點,當把Hive metastore Parquet錶轉換爲Spark SQL Parquet表時,必須將Hive metastore schema和Parquet schema進行一致化。其規則如下:
  • 兩個schema中,忽略空值性,具有相同名字的字段必須具有相同的數據類型。一致化後的字段類型應該與Parquet的字段類型一致,以便遵守空值性原則
  • 一致化後的schema只包含在Hive metastore schema定義的字段:
             i. 丟棄只在Parquet schema定義的字段
             i. 把只在Hive metastore schema定義的字段設爲允許爲空

2.4.2 元數據刷新

爲了提高性能,Spark SQL會緩存Parquet元數據(metadata)。當Hive metastore Parquet錶轉換的選項開啓時,轉換後的表元數據也會被緩存。如果這些表被Hive或者其它外部工具更新,則需要手動刷新緩存以確保元數據的一致性。

// spark is an existing SparkSession
spark.catalog().refreshTable("my_table");

2.5 配置

Parquet的配置可以使用SparkSession的setConf方法或者使用SQL執行SET key=value命令。詳細的配置參數如下:



3. JSON Datasets

Spark SQL可以自動推斷JSON數據集的schema並加載爲Dataset<Row>。此轉換可以使用SparkSession.read().json()方法讀取一個String類型的RDD或者一個JSON文件。需要注意的是,這裏的JSON文件不是典型的JSON格式。這裏的JSON文件每一行必須包含一個獨立有效的JSON對象,也稱爲換行符分割JSON文件。因此,一個規則的多行JSON文件會導致讀取出錯。讀取JSON數據集例子如下:

import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;

// A JSON dataset is pointed to by path.
// The path can be either a single text file or a directory storing text files
Dataset<Row> people = spark.read().json("examples/src/main/resources/people.json");

// The inferred schema can be visualized using the printSchema() method
people.printSchema();
// root
//  |-- age: long (nullable = true)
//  |-- name: string (nullable = true)

// Creates a temporary view using the DataFrame
people.createOrReplaceTempView("people");

// SQL statements can be run by using the sql methods provided by spark
Dataset<Row> namesDF = spark.sql("SELECT name FROM people WHERE age BETWEEN 13 AND 19");
namesDF.show();
// +------+
// |  name|
// +------+
// |Justin|
// +------+

// Alternatively, a DataFrame can be created for a JSON dataset represented by
// an RDD[String] storing one JSON object per string.
List<String> jsonData = Arrays.asList(
        "{\"name\":\"Yin\",\"address\":{\"city\":\"Columbus\",\"state\":\"Ohio\"}}");
JavaRDD<String> anotherPeopleRDD =
        new JavaSparkContext(spark.sparkContext()).parallelize(jsonData);
Dataset<Row> anotherPeople = spark.read().json(anotherPeopleRDD);
anotherPeople.show();
// +---------------+----+
// |        address|name|
// +---------------+----+
// |[Columbus,Ohio]| Yin|
// +---------------+----+

* 參考Spark SQL官方鏈接:http://spark.apache.org/docs/latest/sql-programming-guide.html#data-sources

TO BE CONTINUED...O(∩_∩)O
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章