翻譯自Spark官網(wǎng)。
一、Spark Sql 歷史
大數(shù)據(jù)主要包括三類操作:
1、 長時間運行的批量數(shù)據(jù)處理。
2、 交互式運行的數(shù)據(jù)查詢。
3、 實時數(shù)據(jù)流處理。
Spark Sql 的前身是shark,最初是用在查詢Hive的,hive是為熟悉數(shù)據(jù)庫,但是不熟悉MapReduce技術人員提供的工具,hive提供一些列工具,可以對數(shù)據(jù)進行提取轉(zhuǎn)化和加載(簡稱ETL),通過Hive工具可以查詢分析存儲在Hadoop上的大規(guī)模數(shù)據(jù)機制。Hive定義了簡單的查詢語言HQL(類SQL),可以把SQL操作轉(zhuǎn)成MapReduce任務。
但是MapReduce的計算過程消耗大量的IO,降低了運行效率,為了提高SQL-On-Hadoop的效率,出現(xiàn)了大量的工具,包括Impla 和shark。
Shark 是spark的生態(tài)組件之一,它復用了hive的sql解析等組件,修改了內(nèi)存管理,物理計劃、執(zhí)行模塊,使它能夠運行在Spark的計算引擎上,使用SQL的查詢速度有了10-100倍的提升。
隨著shark的發(fā)展,shark對hive的依賴限制了其發(fā)展,包括語法解析器和查詢優(yōu)化器。Spark團隊汲取了shark的優(yōu)點重新設計了Spark Sql,使之在數(shù)據(jù)兼容、性能優(yōu)化、組件擴展等方面得到極大的提升。
數(shù)據(jù)兼容:不僅兼容Hive,還可以從RDD、parquet文件、Json文件獲取數(shù)據(jù)、支持從RDBMS獲取數(shù)據(jù)。
性能優(yōu)化:采用內(nèi)存列式存儲、自定義序列化器等方式提升性能。
組件擴展:SQL的語法解析器、分析器、優(yōu)化器都可以重新定義和擴展。
二、Spark Sql 概述
Spark SQL 是spark中用于處理結(jié)構(gòu)化數(shù)據(jù)的模塊。Spark SQL相對于RDD的API來說,提供更多結(jié)構(gòu)化數(shù)據(jù)信息和計算方法。Spark SQL 提供更多額外的信息進行優(yōu)化。可以通過SQL或DataSet API方式同Spark SQL進行交互。無論采用哪種方法,哪種語言進行計算操作,實際上都用相同的執(zhí)行引擎,因此使用者可以在不同的API中進行切換,選擇一種最自然的方式去執(zhí)行一個轉(zhuǎn)換。
SQL
一種使用Spark SQL 的方法是進行SQL查詢。Spark SQL 可以從存在的Hive中讀取數(shù)據(jù)。當在編程語言中使用SQL的時候,結(jié)果將返回一個DataSet或DataFrame類型封裝的對象。
你也可以通過命令行或JDBC/ODBC方式使用SQL接口。
三、DataFrame和DataSet
DataSet是分布式的數(shù)據(jù)集合。DataSet是在Spark1.6中添加的新的接口。它集中了RDD的優(yōu)點(強類型 和可以用強大lambda函數(shù))以及Spark SQL優(yōu)化的執(zhí)行引擎。DataSet可以通過JVM的對象進行構(gòu)建,可以用函數(shù)式的轉(zhuǎn)換(map/flatmap/filter)進行多種操作。
DataSet API 在Scala和Java中都是可以用的。
DataSet 通過Encoder實現(xiàn)了自定義的序列化格式,使得某些操作可以在無需序列化情況下進行。另外Dataset還進行了包括Tungsten優(yōu)化在內(nèi)的很多性能方面的優(yōu)化。
DataFrame和DataSet類似,也是個分布式集合,其中數(shù)據(jù)別組織成命名的列,可以看做關系數(shù)據(jù)庫中的表,底層做了很多優(yōu)化,可以通過很多數(shù)據(jù)源進行構(gòu)建,比如RDD、結(jié)構(gòu)化文件、外部數(shù)據(jù)庫、Hive表。
DataFrame的前身是SchemaRDD。Spark1.3開始SchemaRDD更改為DataFrame。區(qū)別,不繼承RDD,自己實現(xiàn)了RDD的大部分功能。可以在DataFrame上調(diào)用RDD的方法轉(zhuǎn)化成另外一個RDD。
DataFrame可以看做分布式Row對象的集合,其提供了由列組成的詳細模式信息,
使其可以得到優(yōu)化。
DataFrame 不僅有比RDD更多的算子,還可以進行執(zhí)行計劃的優(yōu)化。
DataSet包含了DataFrame的功能,Spark2.0中兩者統(tǒng)一,DataFrame表示為DataSet[Row],即DataSet的子集。
使用API盡量使用DataSet ,不行再選用DataFrame,其次選擇RDD。
四、DataFrame基本說明
要使用DataFrame,在2.0中需要SparkSession這個類,創(chuàng)建這個類的方法如下:
import org.apache.spark.sql.SparkSession
val spark = SparkSession
.builder()
.appName("Spark SQL Example")
.config("spark.some.config.option", "some-value")
.getOrCreate()
// For implicit conversions like converting RDDs to DataFrames
import spark.implicits._
通過SparkSession,應用程序可以通過存在的RDD、或者Hive表中或者Spark數(shù)據(jù)源中中創(chuàng)建DataFrame,下面是從JSON文件中創(chuàng)建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|
// +----+-------+
DataFrame 為DataSet[Row],可以執(zhí)行一些非強制類型的轉(zhuǎn)換,例子如下:
// 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只是知道字段,但是不知道字段的類型,所以在執(zhí)行這些操作的時候是沒辦法在編譯的時候檢查是否類型失敗的,比如你可以對一個String進行減法操作,在執(zhí)行的時候才報錯,而DataSet不僅僅知道字段,而且知道字段類型,所以有更嚴格的錯誤檢查。
在程序中使用SQL查詢
在SparkSession中可以用程序的方式運行SQL查詢,結(jié)果作為一個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|
// +----+-------+
五、DataSet使用說明
如上面所述,DataSet不同于RDD,沒有使用Java序列化器或者Kryo進行序列化,而是使用一個特定的編碼器進行序列化,這些序列化器可以自動生成,而且在spark執(zhí)行很多操作(過濾、排序、hash)的時候不用進行反序列化。
創(chuàng)建DataSet
// 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|
// +----+-------+
正如上述,DataSet不光有各個字段名,而且有其詳細的類型,使其在編譯的時候就可以進行錯誤的檢查。
六、與RDD互操作
Spark SQL 支持兩種不同的方法用于將存在的RDD轉(zhuǎn)成Datasets。
第一種方法使用反射去推斷包含特定類型對象的RDD模式(schema),該模式使你的代碼更加簡練,不過你必須在寫Spark的程序的時候已經(jīng)知道模式信息(比如RDD中的對象是自己定義的case class類型)。
第二種方法是通過一個編程接口,此時你需要構(gòu)造一個模式,將其應用到一個已經(jīng)存在的RDD上將其轉(zhuǎn)化為DataFrame,該方法適用于運行之前還不知道列以及列的類型。
l 用反射推斷模式
Spark SQL的Scala接口支持將包含case class的RDD自動轉(zhuǎn)換為DataFrame。case class定義了表的模式,case class的參數(shù)名被反射讀取并成為表的列名。case class也可以嵌套或者包含復雜類型(如序列或者數(shù)組)。示例如下:
import org.apache.spark.sql.catalyst.encoders.ExpressionEncoder
import org.apache.spark.sql.Encoder
// 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, Int]] = 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))
l 編程指定模式
當case class不能提前定義時(比如記錄的結(jié)構(gòu)被編碼為字符串,或者當文本數(shù)據(jù)集被解析時不同用戶需要映射不同的字段),可以通過下面三步來將RDD轉(zhuǎn)換為DataFrame:
1、從原始RDD創(chuàng)建得到一個包含Row對象的RDD。
2、創(chuàng)建一個與第1步中Row的結(jié)構(gòu)相匹配的StructType,以表示模式信息。
3、通過createDataFrame()將模式信息應用到第1步創(chuàng)建的RDD上。
舉例說明:
import org.apache.spark.sql.types._
// 1、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"
///2、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))
//3、 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|
// +-------------+
七、數(shù)據(jù)源
DataFrame 可以當做標準的RDD進行操作,也可以注冊為一個臨時表。將DataFrame注冊為一個臨時表,準許你在上執(zhí)行SQL查詢。
DataFrame接口可以處理多種數(shù)據(jù)源,SparkSql 也內(nèi)建若干個有用的數(shù)據(jù)源格式(Json、parquet、jdbc)。此外,當你用SQL查詢的數(shù)據(jù)源的時候,只使用了一部分字段,SparkSQL可以智能掃描這些字段。
標準的加載和保存函數(shù)
在最簡單的方式,默認的數(shù)據(jù)源(parquet 除非在spark.sql.source.default中特殊設置)將被用來執(zhí)行多種操作。
val usersDF = spark.read.load("examples/src/main/resources/users.parquet")
usersDF.select("name", "favorite_color").write.save("namesAndFavColors.parquet")
手工指定選項
你可以通過手工指定數(shù)據(jù)源和任何想要傳遞給數(shù)據(jù)源的選項。指定數(shù)據(jù)源通常需要使用數(shù)據(jù)源全名(如org.apache.spark.sql.parquet),但對于內(nèi)建數(shù)據(jù)源,你也可以使用它們的短名(json、parquet和jdbc)。并且不同的數(shù)據(jù)源類型之間都可以相互轉(zhuǎn)換。
示例如下:
val peopleDF = spark.read.format("json").load("examples/src/main/resources/people.json")
peopleDF.select("name", "age").write.format("parquet").save("namesAndAges.parquet")
在文件上直接運行SQL
除了你可以通過讀文件的API將文件讀入DataFrame 然后查詢它,還可以通過SQL直接查詢文件。
val sqlDF = spark.sql("SELECT * FROM parquet.\
examples/src/main/resources/users.parquet`")`
保存模式
可以通過一個選項來進行設置保存模式 SaveMode,這個選項指定了當數(shù)據(jù)存在的時候如何處理。要認識到這些選項不是用任何鎖,也不是原子性的。此外,當執(zhí)行OverWrite,在寫數(shù)據(jù)之前刪除老數(shù)據(jù)。
Scala/Java | Any Language | Meaning |
---|---|---|
SaveMode.ErrorIfExists(default) | "error"(default) | When saving a DataFrame to a data source,if data already exists, an exception is expected to be thrown. |
SaveMode.Append | "append" | When saving a DataFrame to a data source,if data/table already exists, contents of the DataFrame are expected to be appended to existing data. |
SaveMode.Overwrite | "overwrite" | Overwrite mode means that when saving a DataFrame to a data source, if data/table already exists, existing data is expected to be overwritten by the contents of the DataFrame. |
SaveMode.Ignore | "ignore" | Ignore mode means that when saving a DataFrame to a data source, if data already exists, the save operation is expected to not save the contents of the DataFrame and to not change the existing data. This is similar to a CREATE TABLE IF NOT EXISTS in SQL |
** 保存到持久表**
DataFrame 可以通過saveAsTable命令將數(shù)據(jù)作為持久表保存到Hive的元數(shù)據(jù)中。使用這個功能不一定需要Hive的部署。Spark將創(chuàng)建一個默認的本地的Hive的元數(shù)據(jù)保存(通過用Derby(一種數(shù)據(jù)庫))。不同于createOrReplaceTempView,saveAsTable將實現(xiàn)DataFrame內(nèi)容和創(chuàng)建一個指向這個Hive元數(shù)據(jù)的指針。持久表在你的spark程序重啟后仍然存在,只要你保存你和元數(shù)據(jù)存儲的連接。可以通過SparkSession調(diào)用table這個方法,來將DataFrame保存為一個持久表。
通過默認的saveAsTable 將會創(chuàng)建一個“管理表”,意思是數(shù)據(jù)的位置將被元數(shù)據(jù)控制。在數(shù)據(jù)表被刪除的時候管理表也會被刪除。
Parquet文件
Parquet 格式是被許多其他的數(shù)據(jù)處理系統(tǒng)支持的列數(shù)據(jù)格式類型。Spark Sql支持在讀寫Parquet文件的時候自動保存原始數(shù)據(jù)的模式信息。在寫Parquet文件時候,所有的列將會因為兼容原因轉(zhuǎn)成nullable。
編程方式加載Parquet數(shù)據(jù):
// 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|
// +------------+
Parquet分區(qū)自動發(fā)現(xiàn)
在很多系統(tǒng)中(如Hive),表分區(qū)是一個通用的優(yōu)化方法。在一個分區(qū)的表中,數(shù)據(jù)通常存儲在不同的目錄中,列名和列值通常被編碼在分區(qū)目錄名中以區(qū)分不同的分區(qū)。Parquet數(shù)據(jù)源能夠自動地發(fā)現(xiàn)和推斷分區(qū)信息。 如下是人口分區(qū)表目錄結(jié)構(gòu),其中gender和country是分區(qū)列:
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
將會自動從路徑中提取分區(qū)信息,返回的DataFrame的分區(qū)信息如下:
root
|-- name: string (nullable = true)
|-- age: long (nullable = true)
|-- gender: string (nullable = true)
|-- country: string (nullable = true)
注意上述分區(qū)的數(shù)據(jù)類型是自動推斷的。目前支持數(shù)值類型和string類型。
如果你不想自動推斷分區(qū)列的數(shù)據(jù)類型。自動推斷分區(qū)列是通過spark.sql.sources.partitionColumnTypeInference.enabled,選項,默認為ture。當類型推斷不可用時候,自動指定分區(qū)的列為string類型。
從spark1.6開始、分區(qū)默認只發(fā)現(xiàn)給定路徑下的分區(qū)。比如用戶傳遞/path/to/table/gender=male 作為讀取數(shù)據(jù)路徑,gender將不被作為一個分區(qū)列。你可以在數(shù)據(jù)源選項中設置basePath來指定分區(qū)發(fā)現(xiàn)應該開始的基路徑,那樣像上述設置,gender將會被作為分區(qū)列。
Parquet模式合并
就像ProtocolBuffer、Avro和Thrift,Parquet也支持模式演化(schema evolution)。這就意味著你可以向一個簡單的模式逐步添加列從而構(gòu)建一個復雜的模式。這種方式可能導致模式信息分散在不同的Parquet文件中,Parquet數(shù)據(jù)源能夠自動檢測到這種情況并且合并所有這些文件中的模式信息。
但是由于模式合并是相對昂貴的操作,并且絕大多數(shù)情況下不是必須的,因此從Spark 1.5.0開始缺省關閉模式合并。開啟方式:在讀取Parquet文件時,
1、 設置數(shù)據(jù)源選項mergeSchema為true,
2、 或者設置全局的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)
Parquet表和Hive元數(shù)據(jù)轉(zhuǎn)換
暫時忽略。
JSON DataSet
Spark SQL 可以自動推斷出JSON 數(shù)據(jù)集的模式,把它加載為一個DataSet[Row].在通過SparkSession.read.json()讀取一個String類型的RDD或者一個JSON文件。
注意: 這里面的Json每一行必須是一個有效的JSOn對象,如果一個對象跨越多行將導致失敗。
舉例:
*// 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*
*// an RDD[String] storing one JSON object per string*
**val** otherPeopleRDD **=** spark.sparkContext.makeRDD(
"""{"name":"Yin","address":{"city":"Columbus","state":"Ohio"}}""" :: **Nil**)
**val** otherPeople **=** spark.read.json(otherPeopleRDD)
otherPeople.show()
*// +---------------+----+*
*// | address|name|*
*// +---------------+----+*
*// |[Columbus,Ohio]| Yin|*
*// +---------------+----+*
otherPeople.printSchema()
root
|-- address: struct (nullable = true)
| |-- city: string (nullable = true)
| |-- state: string (nullable = true)
|-- name: string (nullable = true)
Hive Tables
忽略
和不同Hive 版本交互
忽略