一 概述
spark sql是结构化数据处理模块,可以通过SQL语句和Dataset API进行结构化数据处理。
1.1 SQL
spark sql一个用途就是sql查询,也可以读取已经存在的hive仓库的数据。程序中运行sql语句,将会返回Dataset/DataFrame数据结构。你也可以通过使用spark-sql命令行或jdbc/odbc服务进行sql操作。
1.2 Datasets和DataFrames
Dataset是分布式数据集,spark1.6版本之后新加的一个接口。DataSet是有类型的一个数据集合,例如DataSet<Pserson>,所以在对数据集记录操作时,就可以使用类似person.getName()这种操作来保证类型安全。DataFrame可以从结构化数据文件、hive表、外部数据库或者已经存在的RDD得到。DataFrame是无类型的,其实就是DataSet<Row>,访问记录时可以使用row.getString(0)这种api访问,不保证类型安全,例如这一行的第一个字段没有办法转成String。DataFrame是有schema的,DataSet没有,schema定义了每行数据的数据结构,类型与表的字段信息。
二 入门指南
2.1 切入点
spark sql的切入点是SparkSession类,如下方式创建:
import org.apache.spark.sql.SparkSession
val spark = SparkSession
.builder()
.appName("Spark SQL basic example")
.config("spark.some.config.option", "some-value")
.getOrCreate()
// For implicit conversions like converting RDDs to DataFrames(如果想用RDD的toDF方法转换成DataFrames,那么就需要如下操作)
import spark.implicits._
spark2.0之后的SparkSeesion就提供了HiveQL查询、hive UDFs使用、从hive表读取数据的支持。你不需要安装hive就可以使用这些特性。
2.2 创建DataFrames
可以从已存在的RDD、hive表或其他spark数据源创建DataFrame。如下是从json文件创建:
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|
// +----+-------+
2.3 无类型的Dataset操作
如下示例(其实就是DataFrame的一些操作):
// 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|
// +----+-----+
2.4 程序中sql查询
如下:
df.createOrReplaceTempView("people")//创建临时表,程序退出后清除
val sqlDF = spark.sql("SELECT * FROM people")//从临时表做一些sql查询操作
sqlDF.show()
// +----+-------+
// | age| name|
// +----+-------+
// |null|Michael|
// | 30| Andy|
// | 19| Justin|
// +----+-------+
sql函数返回一个DataFrame作为查询结果。
2.5 全局临时表
临时表是session级别的,创建它的session结束后,临时表会消失。如果你想创建一个跨session(一个application可以有多个SparkSession,session之间上下文环境、资源隔离)共享的临时表,这个临时表在程序退出之前都是存活的。那么你就可以创建一个全局的临时表。全局临时表会存放在global_temp数据库中,访问时需要指定此数据库,如下:
// 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 创建Datasets
dataset和rdd是相似的,但不同于rdd的java序列化器或者kryo,它使用了一个专门的Encoder来对对象进行序列化操作。同样是要把对象序列化成bytes,Encoder能够在不反序列化成对象的情况下,可以做很多类似于过滤、排序、hash操作。
如下使用实例:
case class Person(name: String, age: Long)
// Encoders are created for case classes(针对Person case class创建了encoder)
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
//DataFrame可以用一个类转成DataSet,用类的成员名称进行映射。(people.json里的json格式,包含name和age两项,和case class中的成员名一一对应)
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|
// +----+-------+
2.7 和RDDs交互
两种方式可以把RDDs转成Datasets:
使用反射进行schema推断:条件是,schema已知。
程序中指定schema:程序运行之前是不知道schema信息的。
2.7.1 使用反射进行schema推断
spark sql支持自动把包含case class的RDD转成DataFrame。case class就蕴含了表的schema信息。case class的成员名称就会变成列名。case class也可以支持嵌套和一些复杂的数据类型(Seqs和Arrays)。所以RDD可以转成DataFrame,然后注册成一个表,然后进行一些sql操作,如下实例:
// For implicit conversions from RDDs to DataFrames(导入这个后面才能使用toDF把RDD转成DF)
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(因为之前没有Dataset[Map[K,V]]的encoder,所以要显示定义,不然下面的map会报错)
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, Any]] = 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))
2.7.2 程序中指定schema
有些情况下没有办法定义case class(例如,程序运行之前是不知道schema的,是通过动态参数传入一个字符串或别的途径,是动态读取的schema信息,或者根据用户不同,schema也不同。之前做过一个应用,读取不同接口的数据进行处理,为了适应不同接口的数据schema,就需要把各个接口数据的schema通过参数传到程序中,然后动态的去解析),步骤如下:
1.首先从原始RDD创建RDD<Row>(因为数据结构是已知的,比如有多少个字段)。
2.对应第一个步的RDD<Row>创建StructType(动态解析出字段名和字段类型等信息),这个就是schema信息。
3.通过SparkSession的createDataFrame方法应用schema到RDD<Row>上创建DataFrame。
实例如下:
import org.apache.spark.sql.types._
// Create an RDD(原始的普通RDD)
val peopleRDD = spark.sparkContext.textFile("examples/src/main/resources/people.txt")
// The schema is encoded in a string(这个是schema信息的字段名,这里直接写死,一般会从参数或者其他途径传入)
val schemaString = "name age"
// Generate the schema based on the string of schema(根据schemaString创建对应的schema信息)
val fields = schemaString.split(" ")
.map(fieldName => StructField(fieldName, StringType, nullable = true))
val schema = StructType(fields)
// Convert records of the RDD (people) to Rows(创建RDD<Row>)
val rowRDD = peopleRDD
.map(_.split(","))
.map(attributes => Row(attributes(0), attributes(1).trim))
// Apply the schema to the RDD(应用schema,创建DataFrame)
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|
// +-------------+
2.8 聚合
DataFrames提供例如count(),countDistinct(),avg(),max(),min()等聚合操作。同时,spark sql也提供类型安全的这些操作(Dataset函数)。同样,用户也可以自定义自己的聚合函数。
2.8.1 用户自定义无类型聚合函数(UDAF)
通过继承UserDefinedAggregateFunction实现,如下所示:
import org.apache.spark.sql.{Row, SparkSession}
import org.apache.spark.sql.expressions.MutableAggregationBuffer
import org.apache.spark.sql.expressions.UserDefinedAggregateFunction
import org.apache.spark.sql.types._
object MyAverage extends UserDefinedAggregateFunction {
// Data types of input arguments of this aggregate function(聚合函数传入参数的数据类型)
def inputSchema: StructType = StructType(StructField("inputColumn", LongType) :: Nil)
// Data types of values in the aggregation buffer(聚合过程中buffer的数据类型)
def bufferSchema: StructType = {
StructType(StructField("sum", LongType) :: StructField("count", LongType) :: Nil)
}
// The data type of the returned value(聚合函数返回的数据类型)
def dataType: DataType = DoubleType
// Whether this function always returns the same output on the identical input
def deterministic: Boolean = true
//初始化buffer
// 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.
def initialize(buffer: MutableAggregationBuffer): Unit = {
buffer(0) = 0L
buffer(1) = 0L
}
// Updates the given aggregation buffer `buffer` with new input data from `input`(用传入的数据更新buffer中的值,例如一个task刚开始buffer值都是0,然后对分区记录一条条进行处理,这里的input就是需处理的记录)
def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
if (!input.isNullAt(0)) {
buffer(0) = buffer.getLong(0) + input.getLong(0)
buffer(1) = buffer.getLong(1) + 1
}
}
// Merges two aggregation buffers and stores the updated buffer values back to `buffer1`(聚合不同的buffer,可以理解为各个分区最终都会有一个buffer值,多个task的buffer要进行merge才能得到最终的)
def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
buffer1(0) = buffer1.getLong(0) + buffer2.getLong(0)
buffer1(1) = buffer1.getLong(1) + buffer2.getLong(1)
}
// Calculates the final result(返回最终的结果)
def evaluate(buffer: Row): Double = buffer.getLong(0).toDouble / buffer.getLong(1)
}
// Register the function to access it(自定义聚合函数后要进行注册)
spark.udf.register("myAverage", MyAverage)
val 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|
// +-------+------+
val result = spark.sql("SELECT myAverage(salary) as average_salary FROM employees")
result.show()
// +--------------+
// |average_salary|
// +--------------+
// | 3750.0|
// +--------------+
2.8.2 类型安全的用户自定义聚合函数(UDAF)
通过继承Aggregator抽象类实现,如下:
import org.apache.spark.sql.{Encoder, Encoders, SparkSession}
import org.apache.spark.sql.expressions.Aggregator
case class Employee(name: String, salary: Long)
case class Average(var sum: Long, var count: Long)
//这里直接指定了需要的数据类型(Employee是需要聚合的记录的数据结构,Average是自定义的聚合过程中buffer数据类型,Double是最终返回的数据类型)
object MyAverage extends Aggregator[Employee, Average, Double] {
// A zero value for this aggregation. Should satisfy the property that any b + zero = b(初始0值)
def zero: Average = 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(聚合每个记录,类似于上面的update函数)
def reduce(buffer: Average, employee: Employee): Average = {
buffer.sum += employee.salary
buffer.count += 1
buffer
}
// Merge two intermediate values(类似于上面的merge函数)
def merge(b1: Average, b2: Average): Average = {
b1.sum += b2.sum
b1.count += b2.count
b1
}
// Transform the output of the reduction(最终的结果)
def finish(reduction: Average): Double = reduction.sum.toDouble / reduction.count
// Specifies the Encoder for the intermediate value type(因为类型安全的Dataset是需要指定encode的,这里指定Average类型buffer的encoder)
def bufferEncoder: Encoder[Average] = Encoders.product
// Specifies the Encoder for the final output value type(指定结果数据的encoder)
def outputEncoder: Encoder[Double] = Encoders.scalaDouble
}
val ds = spark.read.json("examples/src/main/resources/employees.json").as[Employee]
ds.show()
// +-------+------+
// | name|salary|
// +-------+------+
// |Michael| 3000|
// | Andy| 4500|
// | Justin| 3500|
// | Berta| 4000|
// +-------+------+
// Convert the function to a `TypedColumn` and give it a name(把聚合函数转成一个字段类型并命名)
val averageSalary = MyAverage.toColumn.name("average_salary")
val result = ds.select(averageSalary)
result.show()
//可以看到这里聚合列名是上面命名的
// +--------------+
// |average_salary|
// +--------------+
// | 3750.0|
// +--------------+
三 数据源
通过DataFrame接口 spark sql支持多种数据源。可以在DataFrame上做一些数据处理转换,同时可以注册为一个临时视图,然后就可以通过这个视图在其数据上做sql查询。
3.1 通用Load/Save方法
默认数据源是parquet格式数据文件(也可以通过spark.sql.sources.default重新指定),如下:
val usersDF = spark.read.load("examples/src/main/resources/users.parquet")
usersDF.select("name", "favorite_color").write.save("namesAndFavColors.parquet")
3.1.1 手动指定数据源配置选项
可以用额外的一些配置选项手动指定数据源,数据源应该用全路径名称指定,但内建的数据源支持简写(json、parquet、jdbc、orc、libsvm、csv、text)。
加载json文件可以如下使用:
val peopleDF = spark.read.format("json").load("examples/src/main/resources/people.json")
peopleDF.select("name", "age").write.format("parquet").save("namesAndAges.parquet")
加载csv文件可以如下使用:
val peopleDFCsv = spark.read.format("csv")
.option("sep", ";")(指定分隔符)
.option("inferSchema", "true")(是否开启schema推断)
.option("header", "true")(是否有头部信息,就是开头一行是字段头信息,不是真正的数据)
.load("examples/src/main/resources/people.csv")
3.1.2 文件上直接运行sql
你可以直接在文件上运行sql,而不用先读到DataFrame中然后查询,如下:
val sqlDF = spark.sql("SELECT * FROM parquet.`examples/src/main/resources/users.parquet`")
parquet指定了文件的格式,所以可以直接进行查询,json的应该写成json.`文件名`,待测试。
3.1.3 保存模式
save操作可以指定SaveMode,需要注意的是,这些保存模式,都没有使用锁机制,所以不是原子操作。
SaveMode.ErrorIfExists(默认的模式):保存DataFrame时,如果数据已经存在,则会抛出异常。
SaveMode.Append:追加到已经存在的数据上。
SaveMode.Overwrite:已经存在的数据先删除,然后写新数据
SaveMode.Ignore:如果数据已经存在,那么忽略本次操作,并不会改变已有数据
3.1.4 保存为持久化的表(非临时表)
可以用saveAsTable保存DataFrames到hive仓库。需要注意的是:如果你没有安装hive,也可以使用这个特性。spark会用Derby来为你创建本地的hive仓库。程序退出重新执行,你持久化的表还是存在的,可以通过SparkSession的table方法读取到DataFrame中。
基于文件的数据源,保存为持久化表时,可以指定保存路径(df.write.option(“path”,“/some/path”).saveAsTable(“t”))。当表被删除了,这个路径和数据还是在的(由此看出这里指定path后是以一个外部表来处理的)。如果没有指定path,那么数据会默认放在仓库目录下,如果表被删除了,默认路径也会被移除(内部表)。
从spark 2.1开始,持久化数据表时,在hive metastore中每个分区都会有对应的元数据存储。这样就带来了几点好处:
1.在查询个别分区的时候,可以只查找返回必要的分区,而不是第一次查询时要遍历所有分区找到需要的分区,这里就可以直接定位查询了。
2.可以使用Hive类似ALTER TABLE PARTITION....SET LOCATION等的DDLs。
需要注意的是,外部表(指定path的)默认分区信息不会被收集管理。如果需要同步仓库中的分区信息(比如做了上面2操作),需要调用MSCK来修复hive表。
3.1.5 桶、排序、分区
对于文件数据源,你还可以对输出进行桶、排序、分区操作。桶和排序操作仅适用于持久化表(saveAsTable)。
如下:
peopleDF.write.bucketBy(42, "name").sortBy("age").saveAsTable("people_bucketed")
分区是save和saveAsTable都可以用的:
usersDF.write.partitionBy("favorite_color").format("parquet").save("namesPartByColor.parquet")
可能一个表会既用到分区,又要用到桶:
usersDF
.write
.partitionBy("favorite_color")
.bucketBy(42, "name")
.saveAsTable("users_partitioned_bucketed")
如果数据基数比较大,分区列类型较多,所以partitionBy后分区会很多,这样其实作用不是很大。而桶限定了桶的数量,所以不管数据基数有多大,索引的消耗是一定的。
3.2 parquet文件
parquet文件格式是很多数据处理系统都支持的,所以spark sql提供了对parquet文件的读写支持,并且会自动保持原始数据的schema信息。当写parquet文件时,我了兼容性的原因,所有的列自动转成可为空的。
3.2.1 编程导入数据
// 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|
// +------------+
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
└── ...
上面的数据,使用SparkSession.read.parquet或SparkSession.read.load方法(传入path/to/table)去读,spark sql会自动去提取path/to/table路径下的分区信息,读取后DataFrame的schema信息就变成了如下:
root
|-- name: string (nullable = true)
|-- age: long (nullable = true)
|-- gender: string (nullable = true)
|-- country: string (nullable = true)
需要注意的是,分区列的数据类型是自动推断的。现在,支持数值、date、timestamp、字符串类型。有时候用户可能不需要分区列的数据类型自动推断。这个时候可以通过设置spark.sql.sources.partitionColumnTypeInference.enabled来实现。当设置成false后,分区列的类型会是string类型。
从spark1.6.0开始,只会默认发现给定路径下的分区。上面的例子,如果用户传path/to/table/gender=male给读取函数,gender就不会作为一个分区列来处理了。如果用户一定要这么做,那么可以指定basePath来实现:option函数指定basePath为path/to/table/,然后gender就是一个分区列了。
3.2.3 schema合并
schema可以演进,刚开始很简单,然后后面逐渐添加更多的列到这个schema。parquet文件如果是不同但兼容的,那么就能够自动发现并合并这些文件。
schema合并是相对昂贵的一个操作。所以spark1.5.0以后,默认关闭了,可以通过以下方法开启:
1.读取parquet文件时通过option函数设置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")//(分区目录存储,key为分区列)
// 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()
//(比如这里value列必须是兼容的,都是数值型,因为这里自动类型推断)
// 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 parquet表元数据转换
待写···(没tm看懂)
3.2.5 元数据更新
spark sql会缓存parquet的元数据信息,所以在做hive parquet元数据转换的时候,转换后的变的元数据也被缓存了。但如果这些表被hive或别的外部工具更新了,那么就要对表进行更新操作以保持一致的元数据信息:
// spark is an existing SparkSession
spark.catalog.refreshTable("my_table")
3.2.6 配置
parquet的配置信息可以通过SparkSession的setConf函数或者在sql中使用set key=value命令设置:
详细配置参见官方文档。
3.3 ORC文件
意思就是支持ORC文件了,具体没有了解过,也没用到过,用到时再看
3.4 JSON数据集
spark sql可以自动推断json数据集的schema并导成Dataset[Row]。可以用SparkSession.read.json()函数来导入。数据源可以是一个String类型的Dataset或JSON文件。
注意,这里的json文件不是一个常规典型的json文件。这里的json文件每一行必须包含一个分隔完整有效的json对象。
多行json文件,需要设置multiLine为true。
一般的json文件如下所示:
{ "people": [
{
"name": "aboutyun",
"age": "4"
},
{
"name": "baidu",
"age": "5"
}
]
}
如果是这种形式,读进来后不但不会读取到schema信息,而且action操作执行后会报错:
根据官方文档,需要设置multiLine,如下所示:
可以看到解释成了array<struct<age:string,name:string>>字段people,和我们的需求也不一致。我们需求是解析成age、name两个字段的DataFrame,所以json文件需要如下所示:
[
{
"name": "aboutyun",
"age": "4"
},
{
"name": "baidu",
"age": "5"
}
]
然后读取后如下所示(满足我们需求):
但是如果没有设置multiFile,那么这种多行的json文件就解析不了了,一个json对象必须写在一行,而且如果一行包含多个json文件,必须是分隔开并且完整的json对象,如下内容的json文件是可以解析出的:
用法实例如下:
// 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(通过string类型的dataset创建,每行是一个json对象的字符串)
// 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表
spark sql支持读写存储在hive中的表。但因为hive的依赖太多,而这些依赖spark中默认不包含的。如果classpath中能够找到hive的依赖,那么spark会自动加载。需要注意的是,由于访问hive中数据的时候,worker节点需要对数据进行序列化和反序列化,所以worker节点也是需要这些hive依赖的。
配置hive:把hive-site.xml(hive配置)、core-site.xml(安全配置)和hdfs-site.xml(hdfs配置)放到spark的conf目录下。(这样初始化SparkSession后,会自动加载现有hive仓库的配置(hive-site.xml),使用sql函数的操作就是基于你已经安装的hive仓库了,如果没有这些拷贝配置文件的操作,spark会通过derby本地创建仓库)
如果没有拷贝hive-site.xml,那么spark会自动在当前目录创建metastore_db,然后创建spark.sql.warehouse.dir配置的仓库目录。(注意,spark2.0.0后,hive-site.xml中hive.metastore.warehouse.dir弃用了,需要用spark.sql.warehouse.dir来指定仓库中数据库的默认位置),启动spark的用户需要有这些目录的权限。
使用实例如下(spark shell中亲测有效):
import java.io.File
import org.apache.spark.sql.{Row, SaveMode, SparkSession}
case class Record(key: Int, value: String)
// warehouseLocation points to the default location for managed databases and tables
val warehouseLocation = new File("spark-warehouse").getAbsolutePath
val spark = SparkSession
.builder()
.appName("Spark Hive Example")
.config("spark.sql.warehouse.dir", warehouseLocation)
.enableHiveSupport()
.getOrCreate()
import spark.implicits._
import spark.sql
sql("CREATE TABLE IF NOT EXISTS src (key INT, value STRING) USING hive")
sql("LOAD DATA LOCAL INPATH 'examples/src/main/resources/kv1.txt' INTO TABLE src")
// Queries are expressed in HiveQL
sql("SELECT * FROM src").show()
// +---+-------+
// |key| value|
// +---+-------+
// |238|val_238|
// | 86| val_86|
// |311|val_311|
// ...
// Aggregation queries are also supported.
sql("SELECT COUNT(*) FROM src").show()
// +--------+
// |count(1)|
// +--------+
// | 500 |
// +--------+
// The results of SQL queries are themselves DataFrames and support all normal functions.
val sqlDF = sql("SELECT key, value FROM src WHERE key < 10 ORDER BY key")
// The items in DataFrames are of type Row, which allows you to access each column by ordinal.
val stringsDS = sqlDF.map {
case Row(key: Int, value: String) => s"Key: $key, Value: $value"
}
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.
val recordsDF = spark.createDataFrame((1 to 100).map(i => Record(i, s"val_$i")))
recordsDF.createOrReplaceTempView("records")
// Queries can then join DataFrame data with data stored in Hive.
sql("SELECT * FROM records r JOIN src s ON r.key = s.key").show()
// +---+------+---+------+
// |key| value|key| value|
// +---+------+---+------+
// | 2| val_2| 2| val_2|
// | 4| val_4| 4| val_4|
// | 5| val_5| 5| val_5|
// ...
// Create a Hive managed Parquet table, with HQL syntax instead of the Spark SQL native syntax
// `USING hive`
sql("CREATE TABLE hive_records(key int, value string) STORED AS PARQUET")
// Save DataFrame to the Hive managed table
val df = spark.table("src")
df.write.mode(SaveMode.Overwrite).saveAsTable("hive_records")
// After insertion, the Hive managed table has data now
sql("SELECT * FROM hive_records").show()
// +---+-------+
// |key| value|
// +---+-------+
// |238|val_238|
// | 86| val_86|
// |311|val_311|
// ...
// Prepare a Parquet data directory
val dataDir = "/tmp/parquet_data"
spark.range(10).write.parquet(dataDir)
// Create a Hive external Parquet table
sql(s"CREATE EXTERNAL TABLE hive_ints(key int) STORED AS PARQUET LOCATION '$dataDir'")
// The Hive external table should already have data
sql("SELECT * FROM hive_ints").show()
// +---+
// |key|
// +---+
// | 0|
// | 1|
// | 2|
// ...
// Turn on flag for Hive Dynamic Partitioning
spark.sqlContext.setConf("hive.exec.dynamic.partition", "true")
spark.sqlContext.setConf("hive.exec.dynamic.partition.mode", "nonstrict")
// Create a Hive partitioned table using DataFrame API
df.write.partitionBy("key").format("hive").saveAsTable("hive_part_tbl")
// Partitioned column `key` will be moved to the end of the schema.
sql("SELECT * FROM hive_part_tbl").show()
// +-------+---+
// | value|key|
// +-------+---+
// |val_238|238|
// | val_86| 86|
// |val_311|311|
// ...
spark.stop()
3.5.1 指定hive表存储格式
当你创建hive表时,你需要指明怎么从文件系统里读取或写入数据,例如指明“input format”和“output format”。你也可以通过指定“serde”来指定数据怎么反序列化成rows或序列化为数据。可以通过“create table src(id int) using hive options(fileFormat ‘parquet’)”这种option指定。我们默认以文本形式读取表数据。需要注意的是,目前创建表时还不hive storage handler,如果你需要,那么需要再hive中进行此操作,然后用spark sql去读取并做相应处理。
具体格式配置,参见官方文档。
3.5.2 对不同版本hive metastore的支持
对于spark sql来说,和hive的metastore进行交互是最重要的,这样spark sql就可以访问hive表的元数据信息了。在spark1.4.0之后,可以通过以下配置来配置所使用的hive metastore的版本信息。(本spark版本,默认支持1.2.1版本hive)。
(对于hive的支持,不是所有的版本都支持的,如果不支持的版本,就会有各种兼容性问题,因此如果需要,可查询官方文档对hive 的版本支持)(hvie支持0.12.0-1.2.1)
3.6 JDBC
spark sql也支持把别的数据库通过jdbc的方式作为自己的一个数据源。如果需要使用此特性,那么首先要指定jdbc的driver class以及相关包,例如命令行方式启动:spark-shell --driver-class-path postgresql-9.4.1207.jar --jars postgresql-9.4.1207.jar。然后还要指定数据的连接配置,比如user和password信息等,相关配置如下参考官方文档。
如下实例:
// Note: JDBC loading and saving can be achieved via either the load/save or jdbc methods
// Loading data from a JDBC source
val jdbcDF = spark.read
.format("jdbc")
.option("url", "jdbc:postgresql:dbserver")
.option("dbtable", "schema.tablename")
.option("user", "username")
.option("password", "password")
.load()
val connectionProperties = new Properties()
connectionProperties.put("user", "username")
connectionProperties.put("password", "password")
val jdbcDF2 = spark.read
.jdbc("jdbc:postgresql:dbserver", "schema.tablename", connectionProperties)
// Specifying the custom data types of the read schema
connectionProperties.put("customSchema", "id DECIMAL(38, 0), name STRING")
val jdbcDF3 = 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)
四 性能调优
4.1 内存缓存数据
可以通过spark.catalog.cacheTable("tableName")或者dataFrame.cache()来对表进行缓存。内存缓存的一些配置可以通过SparkSession的setConf方法或者通过sql里set key=value进行配置。具体参数参考官方文档。
4.2 其他优化配置
还有一些能够优化spark查询的效率的配置,具体参考官方文档。
4.3 sql查询广播变量
意思是可以在join操作时,把表给广播出去,如下:
import org.apache.spark.sql.functions.broadcast
broadcast(spark.table("src")).join(spark.table("records"), "key").show()
然后具体没tm看懂···用到时再研究
五 分布式sql引擎
spark sql可以通过jdbc/odbc server和命令行接口作为一个sql引擎,这样最终用户或者其他应用就可以直接在上面跑sql,而不用去编写spark sql程序了。
5.1 thrift jdbc/odbc server
./sbin/start-thriftserver.sh启动jdbc/odbc server(默认localhost:10000),可以通过--hiveconf指定hive的配置(如果你已经配置过hive相关信息,则一般不需要指定,除非你需要更改一些配置),也可以修改默认的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>
...
然后你就可以通过spark或hive的beeline进行连接测试(只是一个连接测试的客户端。)
例如:
./spark/sbin/start-thriftserver.sh --master yarn启动后,hadoop的8080端口如下所示:
可以看到jdbc/odbc server是跑在yarn上的。然后访问spark的4040端口,可以看到监控页面(每个sql执行情况):
jdbc server也支持通过http传送thrift rpc消息,在conf目录中的hive-site中如下配置(默认是tcp):
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进行测试,如下:
beeline> !connect jdbc:hive2://<host>:<port>/<database>?hive.server2.transport.mode=http;hive.server2.thrift.http.path=<http_endpoint>
5.2 spark sql cl
命令行输入sql进行sql查询的工具。需要注意的是,spark sql cli不能连接thrift jdbc server。所以这个一般会当做测试工具,如果有输入sql进行大数据查询需求,一般会使用thrift jdbc server。
通过./bin/spark-sql运行(可通过--help查看用法,可指定--master)。
hive配置参考上面hive数据源部分。
yarn模式运行后,可在yarn上看到监控,4040上可看到如下:
六 PySpark
不会py·······所以没需求,有需求时再研究
七 迁移指南
具体有需求时研究