1 概述
- Spark1.2中,Spark SQL开始正式支持外部数据源。Spark SQL开放了一系列接入外部数据源的接口,来让开发者可以实现。使得Spark SQL可以加载任何地方的数据,例如mysql,hive,hdfs,hbase等,而且支持很多种格式如json, parquet, avro, csv格式。我们可以开发出任意的外部数据源来连接到Spark SQL,然后我们就可以通过外部数据源API了进行操作。
- 我们通过外部数据源API读取各种格式的数据,会得到一个DataFrame,这是我们熟悉的方式啊,就可以使用DataFrame的API或者SQL的API进行操作哈。
- 外部数据源的API可以自动做一些列的裁剪,什么叫列的裁剪,你如一个user表有id,name,age,gender4个列,在做select的时候你只需要id,name这几个列,那么其他列会通过底层的优化去给我们裁剪掉。
- 保存操作可以选择使用SaveMode,指定如何保存现有数据(如果存在)。
2 读取json文件
启动shell进行测试
//标准写法
val df=spark.read.format("json").load("path")
//另外一种写法
spark.read.json("path")
看看源码这两者之间到底有啥不同呢?
/**
* Loads a JSON file and returns the results as a `DataFrame`.
*
* See the documentation on the overloaded `json()` method with varargs for more details.
*
* @since 1.4.0
*/
def json(path: String): DataFrame = {
// This method ensures that calls that explicit need single argument works, see SPARK-16009
json(Seq(path): _*)
}
我们调用josn() 方法其实进行了 overloaded ,我们继续查看
def json(paths: String*): DataFrame = format("json").load(paths : _*)
这句话是不是很熟悉,其实就是我们的标准写法
scala> val df=spark.read.format("json").load("file:///opt/software/spark-2.2.0-bin-2.6.0-cdh5.7.0/examples/src/main/resources/people.json")
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]
df.printSchema
root
|-- age: long (nullable = true)
|-- name: string (nullable = true)
df.show
+----+-------+
| age| name|
+----+-------+
|null|Michael|
| 30| Andy|
| 19| Justin|
+----+-------+
3 读取parquet数据
val df=spark.read.format("parquet").load("file:///opt/software/spark-2.2.0-bin-2.6.0-cdh5.7.0/examples/src/main/resources/users.parquet")
df: org.apache.spark.sql.DataFrame = [name: string, favorite_color: string ... 1 more field]
df.show
+------+--------------+----------------+
| name|favorite_color|favorite_numbers|
+------+--------------+----------------+
|Alyssa| null| [3, 9, 15, 20]|
| Ben| red| []|
+------+--------------+----------------+
4 读取hive中的数据
spark.sql("show tables").show
+--------+----------+-----------+
|database| tableName|isTemporary|
+--------+----------+-----------+
| default|states_raw| false|
| default|states_seq| false|
| default| t1| false|
+--------+----------+-----------+
spark.table("states_raw").show
+-----+------+
| code| name|
+-----+------+
|hello| java|
|hello|hadoop|
|hello| hive|
|hello| sqoop|
|hello| hdfs|
|hello| spark|
+-----+------+
scala> spark.sql("select name from states_raw ").show
+------+
| name|
+------+
| java|
|hadoop|
| hive|
| sqoop|
| hdfs|
| spark|
+------+
5 保存数据
注意:
- 保存的文件夹不能存在,否则报错(默认情况下,可以选择不同的模式):
org.apache.spark.sql.AnalysisException: path file:/home/hadoop/data already exists.;
- 报错成文本格式,只能报错一列,否则报错:
org.apache.spark.sql.AnalysisException: Text data source supports only a single column, and you have 2 columns.;
val df=spark.read.format("json").load("file:///opt/software/spark-2.2.0-bin-2.6.0-cdh5.7.0/examples/src/main/resources/people.json")
//保存
df.select("name").write.format("text").save("file:///home/hadoop/data/out")
结果:
[hadoop@hadoop out]$ pwd
/home/hadoop/data/out
[hadoop@hadoop out]$ ll
total 4
-rw-r--r--. 1 hadoop hadoop 20 Apr 24 00:34 part-00000-ed7705d2-3fdd-4f08-a743-5bc355471076-c000.txt
-rw-r--r--. 1 hadoop hadoop 0 Apr 24 00:34 _SUCCESS
[hadoop@hadoop out]$ cat part-00000-ed7705d2-3fdd-4f08-a743-5bc355471076-c000.txt
Michael
Andy
Justin
//保存为json格式
df.write.format("json").save("file:///home/hadoop/data/out1")
结果
[hadoop@hadoop data]$ cd out1
[hadoop@hadoop out1]$ ll
total 4
-rw-r--r--. 1 hadoop hadoop 71 Apr 24 00:35 part-00000-948b5b30-f104-4aa4-9ded-ddd70f1f5346-c000.json
-rw-r--r--. 1 hadoop hadoop 0 Apr 24 00:35 _SUCCESS
[hadoop@hadoop out1]$ cat part-00000-948b5b30-f104-4aa4-9ded-ddd70f1f5346-c000.json
{"name":"Michael"}
{"age":30,"name":"Andy"}
{"age":19,"name":"Justin"}
上面说了在保存数据是如果目录已经存在,在默认模式下会报错,那我们下面讲解保存的几种模式:
保存模式 | 意义 |
---|---|
SaveMode.ErrorIfExists (default) | 将DataFrame保存到数据源时,如果数据已经存在,则预计会抛出异常。 |
SaveMode.Append | 将DataFrame保存到数据源时,如果数据/表已存在,则DataFrame的内容预计会附加到现有数据。 |
SaveMode.Overwrite | 覆盖模式意味着将DataFrame保存到数据源时,如果数据/表已存在,则预期现有数据将被DataFrame的内容覆盖。 |
SaveMode.Ignore | 忽略模式意味着将DataFrame保存到数据源时,如果数据已经存在,则保存操作不会保存DataFrame的内容,也不会更改现有数据。 这与SQL中的CREATE TABLE IF NOT EXISTS类似。 |
6 读取mysql中的数据
val jdbcDF = spark.read
.format("jdbc")
.option("url", "jdbc:mysql://localhost:3306")
.option("dbtable", "basic01.tbls")
.option("user", "root")
.option("password", "123456")
.load()
scala> jdbcDF.printSchema
root
|-- TBL_ID: long (nullable = false)
|-- CREATE_TIME: integer (nullable = false)
|-- DB_ID: long (nullable = true)
|-- LAST_ACCESS_TIME: integer (nullable = false)
|-- OWNER: string (nullable = true)
|-- RETENTION: integer (nullable = false)
|-- SD_ID: long (nullable = true)
|-- TBL_NAME: string (nullable = true)
|-- TBL_TYPE: string (nullable = true)
|-- VIEW_EXPANDED_TEXT: string (nullable = true)
|-- VIEW_ORIGINAL_TEXT: string (nullable = true)
jdbcDF.show
7 spark SQL操作mysql表数据
CREATE TEMPORARY VIEW jdbcTable
USING org.apache.spark.sql.jdbc
OPTIONS (
url "jdbc:mysql://localhost:3306",
dbtable "basic01.tbls",
user 'root',
password '123456',
driver "com.mysql.jdbc.Driver"
);
查看:
show tables;
default states_raw false
default states_seq false
default t1 false
jdbctable true
select * from jdbctable;
1 1519944170 6 0 hadoop 0 1 page_views MANAGED_TABLE NULL NULL
2 1519944313 6 0 hadoop 0 2 page_views_bzip2 MANAGED_TABLE NULL NULL
3 1519944819 6 0 hadoop 0 3 page_views_snappy MANAGED_TABLE NULL NULL
21 1520067771 6 0 hadoop 0 21 tt MANAGED_TABLE NULL NULL
22 1520069148 6 0 hadoop 0 22 page_views_seq MANAGED_TABLE NULL NULL
23 1520071381 6 0 hadoop 0 23 page_views_rcfile MANAGED_TABLE NULL NULL
24 1520074675 6 0 hadoop 0 24 page_views_orc_zlib MANAGED_TABLE NULL NULL
27 1520078184 6 0 hadoop 0 27 page_views_lzo_index MANAGED_TABLE NULL NULL
30 1520083461 6 0 hadoop 0 30 page_views_lzo_index1 MANAGED_TABLE NULL NULL
31 1524370014 1 0 hadoop 0 31 t1 EXTERNAL_TABLE NULL NULL
37 1524468636 1 0 hadoop 0 37 states_raw MANAGED_TABLE NULL NULL
38 1524468678 1 0 hadoop 0 38 states_seq MANAGED_TABLE NULL NULL
mysql中的tbls的数据已经存在jdbctable表中了。
8 分区推测(Partition Discovery)
表分区是在像Hive这样的系统中使用的常见优化方法。 在分区表中,数据通常存储在不同的目录中,分区列值在每个分区目录的路径中编码。 所有内置的文件源(包括Text / CSV / JSON / ORC / Parquet)都能够自动发现和推断分区信息。 例如,我们创建如下的目录结构;
hdfs dfs -mkdir -p /user/hive/warehouse/gender=male/country=CN
添加json文件:
people.json
{"name":"Michael"}
{"name":"Andy", "age":30}
{"name":"Justin", "age":19}
hdfs dfs -put people.json /user/hive/warehouse/gender=male/country=CN
- 我们使用spark sql读取外部数据源:
val df=spark.read.format("json").load("/user/hive/warehouse/gender=male/country=CN/people.json")
scala> df.printSchema
root
|-- age: long (nullable = true)
|-- name: string (nullable = true)
scala> df.show
+----+-------+
| age| name|
+----+-------+
|null|Michael|
| 30| Andy|
| 19| Justin|
+----+-------+
- 我们改变读取的目录
val df=spark.read.format("json").load("/user/hive/warehouse/gender=male/")
scala> df.printSchema
root
|-- age: long (nullable = true)
|-- name: string (nullable = true)
|-- country: string (nullable = true)
scala> df.show
+----+-------+-------+
| age| name|country|
+----+-------+-------+
|null|Michael| CN|
| 30| Andy| CN|
| 19| Justin| CN|
+----+-------+-------+
大家有没有发现什么呢?Spark SQL将自动从路径中提取分区信息。
注意,分区列的数据类型是自动推断的。目前支持数字数据类型,日期,时间戳和字符串类型。有时用户可能不想自动推断分区列的数据类型。对于这些用例,自动类型推断可以通过spark.sql.sources.partitionColumnTypeInference.enabled
进行配置,默认为true。当禁用类型推断时,字符串类型将用于分区列。
从Spark 1.6.0开始,默认情况下,分区发现仅在给定路径下找到分区。对于上面的示例,如果用户将路径/到/ table / gender = male传递给SparkSession.read.parquet或SparkSession.read.load,则不会将性别视为分区列。如果用户需要指定分区发现应以其开始的基本路径,则可以在数据源选项中设置basePath。例如,当path / to / table / gender = male是数据路径并且用户将basePath设置为path / to / table /时,性别将是分区列。
9 Schema合并(Schema Merging)
与Protocol Buffer,Avro和Thrift一样,Parquet类型的文件也支持Schema合并操作。 用户可以合并多个Parquet文件。 Parquet数据源现在能够自动检测这种情况并合并所有这些文件的模式。
由于模式合并是一种相对昂贵的操作,并且在大多数情况下不是必需的,因此我们默认从1.5.0开始关闭它。 您可以启用它在读取Parquet文件时将数据源选项mergeSchema
设置为true(如下面的示例所示),或将全局SQL选项spark.sql.parquet.mergeSchema
设置为true。
val squaresDF = spark.sparkContext.makeRDD(1 to 5).map(i => (i, i * i)).toDF("value", "square")
squaresDF.write.parquet("/schema_merge/test_table/key=1")
val cubesDF = spark.sparkContext.makeRDD(6 to 10).map(i => (i, i * i * i)).toDF("value", "cube")
cubesDF.write.parquet("/schema_merge/test_table/key=2")
val mergedDF = spark.read.format("parquet").option("mergeSchema", "true").load("/schema_merge/test_table")
mergedDF.printSchema()
mergedDF.show
- 如果不设置
option("mergeSchema", "true")
+-----+------+---+
|value|square|key|
+-----+------+---+
| 1| 1| 1|
| 2| 4| 1|
| 3| 9| 1|
| 4| 16| 1|
| 5| 25| 1|
| 6| null| 2|
| 7| null| 2|
| 8| null| 2|
| 9| null| 2|
| 10| null| 2|
+-----+------+---+
- 如果设置:
+-----+------+----+---+
|value|square|cube|key|
+-----+------+----+---+
| 3| 9|null| 1|
| 4| 16|null| 1|
| 5| 25|null| 1|
| 8| null| 512| 2|
| 9| null| 729| 2|
| 10| null|1000| 2|
| 1| 1|null| 1|
| 2| 4|null| 1|
| 6| null| 216| 2|
| 7| null| 343| 2|
+-----+------+----+---+
- hdfs 上的文件
[root@hadoop ~]# hdfs dfs -ls /schema_merge/test_table
Found 2 items
drwxr-xr-x - hadoop supergroup 0 2018-04-28 21:47 /schem_merge/test_table/key=1
drwxr-xr-x - hadoop supergroup 0 2018-04-28 21:48 /schem_merge/test_table/key=2