简介:
SparkSQL通过DataFrame接口支持处理各种数据源,df可以抽象为RDD或注册内存(临时)表处理,临时表可以通过SQL操作并返回一个结果df。
Load/Save 方法:
最简单的格式,默认为parquet(列式存储格式,自身包含表结构和表数据),可以在spark.sql.sources.default里配置。
例:scala
val df = sqlContext.read.load("examples/src/main/resources/users.parquet")
df.select("name", "favorite_color").write.save("namesAndFavColors.parquet")
手动选择数据源,数据源的名称可以写全(例:org.apache.spark.sql.parquet),也可以简写(json,parquet,jdbc)。DataFrame通过以下语法可以转换为任何格式。
val df = sqlContext.read.format("json").load("examples/src/main/resources/people.json")
df.select("name", "age").write.format("parquet").save("namesAndAges.parquet")
指定保存模式很重要,因为数据没有使用任何锁,也不是原子性的,通常为了避免被覆盖数据会在写入新数据前先清空。
下面是几种保存模式:
Scala/Java | Any Language | Meaning |
---|---|---|
SaveMode.ErrorIfExists (default) |
"error" (default) |
当保存DataFrame到数据文件时, 若数据文件以存在,抛出异常 |
SaveMode.Append |
"append" |
当保存DataFrame到数据文件时, 若数据文件以存在,新的数据内容会加到数据文件的后面 |
SaveMode.Overwrite |
"overwrite" |
当保存DataFrame到数据文件时, 若数据文件以存在,新的数据会覆盖原有数据 |
SaveMode.Ignore |
"ignore" |
当保存DataFrame到数据文件时, 若数据文件以存在,不执行任何操作,类似 CREATE TABLE IF NOT EXISTS in SQL. |
HiveContext也可以通过saveAsTable把DataFrame保存为持久表,不像registerTempTable只是临时内存表,saveAsTable会给HiveMetaStore内写入数据成为真正的表,当你重启spark项目持久表依旧存在。持久表可以通过SQLcontext的table方法创建为DataFrame。
默认情况下,saveAsTable会创建一个管理表,意味着MetaStore管理着真实的数据地址,当表数据被删除,管理表也将删除对应的数据。
Parquet文件:
parquet是一种列式存储格式,支持许多其他的数据处理系统。SparkSQL提供了read和write方法并自动解析保存数据对应的schema.
Loading Data:
例:scala
import sqlContext.implicits._
val people: RDD[Person] = ... // 上一篇博文内的例子
people.write.parquet("people.parquet")
// 读取上面创建的parquet文件 Parquet文件是"自解释"的,即schema不用再提供
// 读取parquet文件后的结果也是DataFrame
val parquetFile = sqlContext.read.parquet("people.parquet")
parquetFile.registerTempTable("parquetFile")
val teenagers = sqlContext.sql("SELECT name FROM parquetFile WHERE age >= 13 AND age <= 19")
teenagers.map(t => "Name: " + t(0)).collect().foreach(println)
表分区是一种常见的系统优化方式例如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
└── ...
通过SQLContext.read.parquet或SQLContext.read.load读取表路径,SparkSQL会自动从路径中提取分区信息。
从DataFrame返回的schema为:
root
|-- name: string (nullable = true)
|-- age: long (nullable = true)
|-- gender: string (nullable = true)
|-- country: string (nullable = true)
注意:分区字段的类型是自动识别的,目前支持numeric和string类型。
有时用户不想让它自动识别,可以通过spark.sql.sources.partitionColumnTypeInference.enabled配置,默认为true。关闭自动识别后,string类型会用于分区字段。
合并Schema
像ProtocolBuffer,Avro,Thrift一样,parquet也支持schema evolution。用户可以从一个简单的schema逐步添加更多的schema,通过这种方式,用户希望能合并多个有不同schema的parquet文件。parquet数据源现在支持自动的检测并合并这些文件。
默认情况下从1.5.0开始是关闭的,可以手动的开启:
方法1.当read parquet文件的时候,设置数据源选项mergeSchema为true(下例子).
方法2.设置全局SQL选项,
import sqlContext.implicits._
// 创建一个简单的DF,写入到分区目录
val df1 = sc.makeRDD(1 to 5).map(i => (i, i * 2)).toDF("single", "double")
df1.write.parquet("data/test_table/key=1")
// 创建另一个DF,写入到另一个分区目录,
// 增加一个新的字段并删除已存在的字段
val df2 = sc.makeRDD(6 to 10).map(i => (i, i * 3)).toDF("single", "triple")
df2.write.parquet("data/test_table/key=2")
// 读取分区表
val df3 = sqlContext.read.option("mergeSchema", "true").parquet("data/test_table")
df3.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
// |-- single: int (nullable = true)
// |-- double: int (nullable = true)
// |-- triple: int (nullable = true)
// |-- key : int (nullable = true)
当对hive metastore parquet table读或写的时候,SparkSQL为了性能会尝试自己支持的的parquet而不是hive提供的。这种行为由spark.sql.hive.convertMetastoreParquet配置控制,默认为开启。
Hive/Parquet Schema 调解:
hive和parquet在处理表结构上有两个关键区别:
1.hive对大小写不敏感,而parquet敏感
2.hive允许字段为空,而parquet对可为空很严格
因此当转换hive metastore parquet table为 Spark SQL parquet table需要协调hive metastore schema和parquet schema .
调解规则为:
1.两个schema中的同名字段不管是否为空,类型必须相同,调解字段的数据类型为parquet的,所以是否为空很重要
2. 只出现在parquet schema中的任何字段都在调解schema中删除。
只出现在hive metastore schema中的任何字段以nullable字段加入到调解schema中。
元数据刷新:
SparkSQL为了更好的性能缓存了parquet metadata。当Hive metastore parquet table的转换为开启的,也会被缓存元数据表。如果这些表被hive或其他的外部工具更新,需要手动的刷新元数据保证元数据一致。
例:scala
// sqlContext is an existing HiveContext
sqlContext.refreshTable("my_table")
parquet的配置可以通过SQLContext的setConf方法或执行set key=balue命令行:
Property Name | Default | Meaning |
---|---|---|
spark.sql.parquet.binaryAsString |
false | 一些其他Parquet-producing systems, 例如 Impala, Hive, 旧版本的 Spark SQL, 在导出parquet schema时不区分二进制数据和string 这个选项告诉Spark SQL把二进制数据当作string来保证兼容性. |
spark.sql.parquet.int96AsTimestamp |
true | 一些 Parquet-producing systems,例如 Impala 和Hive, 存储 Timestamp into INT96. 这个选项告诉Spark SQL把INT96做为timestamp来保证兼容性. |
spark.sql.parquet.cacheMetadata |
true | 开启能缓存parquet schema metadata,当查询静态数据时能提升效率. |
spark.sql.parquet.compression.codec |
gzip | 设置写parquet文件时的压缩码率,可选的值:uncompressed, snappy, gzip, lzo. |
spark.sql.parquet.filterPushdown |
true | 当为true能优化parquet filter push-down |
spark.sql.hive.convertMetastoreParquet |
true | 当为 false, Spark SQL 会使用 Hive SerDe 解析 parquet tables 而不是默认spark提供的 |
spark.sql.parquet.output.committer.class |
org.apache.parquet.hadoop. |
parquet用到的输出类 . 指定的类需要是 通常,它也是 Note:
Spark SQL 有一个内置的类 |
spark.sql.parquet.mergeSchema |
false |
当为 true, parquet数据源从所有数据文件合并schema , 不然schema要从合并文件或随机文件里选择(若没有合并文件). |
Spark SQL可以自动的识别json数据集的schema并且加载为DataFrame。通过SQLContext.read.json()来转换字符串类型RDD或json文件。
注意这里的json不是传统的json文件内容,每一行要包含分隔,独立有效json对象。因此常规的多行json文件通常会失败。
例:scala
// sc is an existing SparkContext.
val sqlContext = new org.apache.spark.sql.SQLContext(sc)
// 指定json文件路径.
val path = "examples/src/main/resources/people.json"
val people = sqlContext.read.json(path)
// 推测出的schema可以打印出来看
people.printSchema()
// root
// |-- age: integer (nullable = true)
// |-- name: string (nullable = true)
// DataFrame注册为临时table.
people.registerTempTable("people")
// sqlContext执行SQL.
val teenagers = sqlContext.sql("SELECT name FROM people WHERE age >= 13 AND age <= 19")
// 二选一, DataFrame也可以通过一个RDD[String]存储了String类型的JSON 对象来创建.
val anotherPeopleRDD = sc.parallelize(
"""{"name":"Yin","address":{"city":"Columbus","state":"Ohio"}}""" :: Nil)
val anotherPeople = sqlContext.read.json(anotherPeopleRDD)
Spark SQL也提供了对Hive表的读和写。然而由于hive本身有大量的依赖关系,因此默认它没有被Spark集成,hive的支持需要在Spark的build中加入 -Phive和 -Phive-thriftserver,这个命令会创建一个新的assembly的包含了hive的jar包。需要注意的是,这个jar包必须在每个worker节点都有,因为它们需要访问hive的序列和反序列库(SerDes)来访问hive数据。
把hive的hive-site.xml文件放到conf/下。注意当在yarn集群运行查询时,在lib_managed/jars目录下的datanucleus jars和conf/目录下的hive-site.xml需要是可用的,所有的executors通过YARN集群启动。简单的方法是在spark-submit 后加--jars和--file。
使用Hive必须构造一个HiveContext,它是SQLContext的子集,增加了在MetaStore中发现表和执行HiveQL的功能,即便没有部署hive也依然可以创建hiveContext。当hive-site.xml没有配置时,context会自动在当前目录创建metastore_db和warehouse。
// sc is an existing SparkContext.
val sqlContext = new org.apache.spark.sql.hive.HiveContext(sc)
sqlContext.sql("CREATE TABLE IF NOT EXISTS src (key INT, value STRING)")
sqlContext.sql("LOAD DATA LOCAL INPATH 'examples/src/main/resources/kv1.txt' INTO TABLE src")
// Queries are expressed in HiveQL
sqlContext.sql("FROM src SELECT key, value").collect().foreach(println)
Spark SQL对hive的支持有一个很重要的目的是与hive metastore互动,允许SparkSQL访问hive表。从Spark1.4.0版本开始,SparkSQL通过以下配置可以被用于查询不同版本的hive metastore。
Property Name | Default | Meaning |
---|---|---|
spark.sql.hive.metastore.version |
1.2.1 |
Version of the Hive metastore. 支持 0.12.0 到1.2.1 . |
spark.sql.hive.metastore.jars |
builtin |
Location of the jars that should be used to instantiate the HiveMetastoreClient. This property can be one of three options:
|
spark.sql.hive.metastore.sharedPrefixes |
com.mysql.jdbc, |
A comma separated list of class prefixes that should be loaded using the classloader that is shared between Spark SQL and a specific version of Hive. An example of classes that should be shared is JDBC drivers that are needed to talk to the metastore. Other classes that need to be shared are those that interact with classes that are already shared. For example, custom appenders that are used by log4j. |
spark.sql.hive.metastore.barrierPrefixes |
(empty) |
A comma separated list of class prefixes that should explicitly be reloaded for each version of Hive that Spark SQL is communicating with. For example, Hive UDFs that are declared in a prefix that typically would be shared (i.e. |
SparkSQL可以通过JDBC读取外部数据库,此方法应该优先于用JdbcRDD。因为它的返回值为DataFrame,可以很方便的进行操作。Jdbc数据源对java和python也同样很方便因为不需要提供一个ClassTag。
需要在Spark classpath指定使用的jdbc driver,例如,在spark shell中连接postgres用以下命令:
SPARK_CLASSPATH=postgresql-9.3-1102-jdbc41.jar bin/spark-shell
Property Name | Meaning |
---|---|
url |
The JDBC URL to connect to. |
dbtable |
The JDBC table that should be read. Note that anything that is valid in a FROM clause of a SQL query can be used. For example, instead of a full table you could also use a subquery in parentheses. |
driver |
The class name of the JDBC driver needed to connect to this URL. This class will be loaded on the master and workers before running an JDBC commands to allow the driver to register itself with the JDBC subsystem. |
partitionColumn, lowerBound, upperBound, numPartitions |
These options must all be specified if any of them is specified. They describe how to partition the table when reading in parallel from multiple workers. partitionColumn must be a numeric column from the table in question. Notice that lowerBound and upperBound are just used to decide the partition stride, not for filtering the rows in table. So all rows in the table will be partitioned and returned. |
val jdbcDF = sqlContext.read.format("jdbc").options(
Map("url" -> "jdbc:postgresql:dbserver",
"dbtable" -> "schema.tablename")).load()