入門
起點: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();
SparkSession在Spark 2.0中爲Hive特性提供內置支持,包括使用HiveQL編寫查詢,訪問Hive UDF以及從Hive表讀取數據的能力。要使用這些功能,您不需要有現有的Hive安裝程序。
創建數據框
使用一個 SparkSession,應用程序可以從現有的RDD,Hive表或Spark數據源創建DataFrame 。
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|
// +----+-------+
無類型數據集操作(又名DataFrame操作)
DataFrames爲Scala,Java,Python和R中的結構化數據操作提供了一個特定領域的語言。
在Spark 2.0中,DataFrames只是RowScala和Java API中的數據集。這些操作也被稱爲“無類型轉換”,與強類型的Scala / Java數據集中的“類型轉換”不同。
一些使用數據集的結構化數據處理的基本示例:
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|
// +----+-------+
除了簡單的列引用和表達式之外,數據集還具有豐富的函數庫,包括字符串操作,日期算術,通用數學運算等等。DataFrame函數參考中提供了完整的列表。
以編程方式運行SQL查詢
SparkSession上的sql函數允許應用程序能以編程方式運行SQL查詢,並將結果返回爲Dataset<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 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|
// +----+-------+
創建數據集
數據集類似於RDD,但是,不使用Java序列化或Kryo,而是使用專門的編碼器對對象進行序列化以便通過網絡進行處理或傳輸。雖然編碼器和標準序列化都負責將對象轉換爲字節,但編碼器是動態生成的代碼,並且使用Spark運行執行的操作(如過濾,排序和散列)格式,而無需將字節反序列化回對象。
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|
// +----+-------+
與RDD進行互操作
Spark SQL支持將現有RDD轉換爲Datasets的兩種不同方法。第一種方法使用反射來推斷包含特定類型對象的RDD的模式。
創建數據集的第二種方法是通過編程接口,允許您構建模式,然後將其應用於現有的RDD。雖然這個方法比較冗長,但是它允許你在構造數據集的時候直到運行時才知道列和它們的類型。
Spark SQL支持自動將JavaBean的RDD 轉換爲DataFrame
Spark SQL不支持包含Map字段的JavaBean 。不過嵌套的JavaBean和Listor Array 字段是受支持的。您可以通過創建一個實現Serializable的類來創建JavaBean,併爲其所有字段設置getter和setter。
// 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|
// +------------+
以編程方式指定模式
當不能提前定義JavaBean類時(例如,記錄的結構是用字符串編碼的,或者文本數據集將被解析,字段對於不同的用戶來說投影會不同),Dataset<Row
>可以用三個步驟以編程方式創建 。
Row從原RDD 創建一個RDD;
在步驟1創建的RDD中StructType與Rows 結構匹配 的模式。
Row通過SparkSession中的createDataFrame提供的方法將模式應用於 的RDD
// 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|
// +-------------+
聚合
該內置功能DataFrames提供聚合,例如count(),countDistinct(),avg(),max(),min(),等。這些功能是專爲DataFrames設計
非類型化的用戶定義的聚合函數
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|
// +--------------+
類型安全的用戶定義的聚合函數
用於強類型數據集的用戶定義聚合關聯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|
// +--------------+
數據源
Spark SQL支持通過DataFrame接口在各種數據源上進行操作。DataFrame可以使用關係變換進行操作,也可以用來創建臨時視圖。將DataFrame註冊爲臨時視圖允許您對其數據運行SQL查詢。本節介紹使用Spark Data Sources加載和保存數據的一般方法,然後介紹可用於內置數據源的特定選項。
通用加載/保存功能
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)。從任何數據源類型加載的數據框可以使用此語法轉換爲其他類型。
Dataset<Row> peopleDF =
spark.read().format("json").load("examples/src/main/resources/people.json");
peopleDF.select("name", "age").write().format("parquet").save("namesAndAges.parquet");
直接在文件上運行SQL
您可以使用SQL直接查詢該文件,而不是使用讀取API將文件加載到DataFrame中並進行查詢。
Dataset<Row> sqlDF =
spark.sql("SELECT * FROM parquet.`examples/src/main/resources/users.parquet`");
保存模式
保存操作可以選擇一個SaveMode,指定如何處理現有的數據(如果存在)。認識到這些保存模式不使用任何鎖定而不是原子是很重要的。另外,執行時Overwrite,數據在寫出新數據之前將被刪除。
SaveMode.ErrorIfExists (默認) "error" (默認) 將DataFrame保存到數據源時,如果數據已經存在,則預計會拋出異常。
SaveMode.Append "append" 將DataFrame保存到數據源時,如果data / table已經存在,則DataFrame的內容將被追加到現有數據中。
SaveMode.Overwrite "overwrite" 覆蓋模式意味着將DataFrame保存到數據源時,如果data / table已經存在,則現有數據將被DataFrame的內容覆蓋。
SaveMode.Ignore "ignore" 忽略模式意味着,當將DataFrame保存到數據源時,如果數據已經存在,保存操作將不會保存DataFrame的內容,也不會更改現有數據。這與CREATE TABLE IF NOT EXISTSSQL中的類似。
保存到持久表
DataFrames也可以使用該saveAsTable 命令將其作爲持久表保存到Hive Metastore中。請注意,現有的Hive部署對於使用此功能不是必需的。Spark將爲您創建一個默認的本地Hive Metastore(使用Derby)。與createOrReplaceTempView命令不同的是, saveAsTable將實現DataFrame的內容並創建指向Hive Metastore中的數據的指針。即使您的Spark程序重新啓動後,持久性表格仍然存在,只要您保持與同一Metastore的連接。用於持久表的DataFrame可以通過使用表的名稱調用tablea方法來創建SparkSession。
對於基於文件的數據源,例如文本,parquet,json等,您可以通過path選項指定自定義表格路徑 ,例如df.write.option(“path”, “/some/path”).saveAsTable(“t”)。當表被刪除時,自定義表路徑將不會被刪除,表數據仍然存在。如果沒有指定自定義表格路徑,則Spark將把數據寫入倉庫目錄下的默認表格路徑。當表被刪除時,默認的表路徑也將被刪除。
持久數據源表具有存儲在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");
而分區則可以同時使用save和saveAsTable使用數據集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,通過固定數量的桶分配數據,並且可以在大量唯一值無界時使用。
分區發現
表分區是像Hive這樣的系統中常用的優化方法。在分區表中,數據通常存儲在不同的目錄中,分區列值在每個分區目錄的路徑中編碼。現在,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。當禁用類型推斷時,字符串類型將用於分區列。
JSON數據集
Spark SQL可以自動推斷JSON數據集的模式,並將其作爲一個Dataset。這個轉換可以SparkSession.read().json()在一個Dataset或者一個JSON文件上完成。
請注意,作爲json文件提供的文件不是典型的JSON文件。每行必須包含一個單獨的,獨立的有效JSON對象。有關更多信息,請參閱 JSON行文本格式,也稱爲換行符分隔的JSON。
對於常規的多行JSON文件,請將該multiLine選項設置爲true。
// 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
// a Dataset<String> storing one JSON object per string.
List<String> jsonData = Arrays.asList(
"{\"name\":\"Yin\",\"address\":{\"city\":\"Columbus\",\"state\":\"Ohio\"}}");
Dataset<String> anotherPeopleDataset = spark.createDataset(jsonData, Encoders.STRING());
Dataset<Row> anotherPeople = spark.read().json(anotherPeopleDataset);
anotherPeople.show();
// +---------------+----+
// | address|name|
// +---------------+----+
// |[Columbus,Ohio]| Yin|
// +---------------+----+
JDBC到其他數據庫
故障排除
性能調整
對於某些工作負載,可以通過在內存中緩存數據或打開一些實驗選項來提高性能。
在內存中緩存數據
其他配置選項
分佈式SQL引擎
運行Thrift JDBC / ODBC服務器
運行Spark SQL CLI
詳情參見
http://spark.apache.org/docs/latest/sql-programming-guide.html