http://spark.apache.org/docs/latest/sql-programming-guide.html
一、概览
Spark SQL是Spark一个用于结构化数据处理的模块。和基础的Spark RDD API不同,Spark SQL提供的接口中提供了更多关于与实际数据和正在执行的计算相关的结构化信息。在内部,Spark SQL这些额外的信息来进行额外的优化计划。目前存在集中和Spark SQL进行交互的方法,包括执o行SQL和Dataset API。当使用统一执行引擎来计算结果时,用户使用的API/语言是相对独立的。这种统一性意味着开发人员可以轻松地在不同的能提供最自然的方式来表达给定的数据转换的API中来回切换。
本文中的全部示例使用Spark发布版本中的样例数据,可以在spark-shell、pyspark shell或是sparkR中执行。
1.1 SQL
Spark SQL中的其中一个用途是用来执行SQL查询。Spark SQL同样可以用于在当前已存在的Hive集群中读取数据。请参考本文中的Hive Tables章节来获取更多关于此特性的详细信息。当使用另外一种编程语言来运行SQL是,结果会以Dataset/DataFrame的格式返回。你同样可以通过使用comman-line或是JDBC/ODBC来和SQL接口进行交互。
1.2 Datasets和DataFrames
一个Dataset是一个分布式的数据集合。Dataset是在Spark1.6版本中添加的新接口,它同时具备RDD的优点(强类型,可使用强大的lambda表达式的能力)和Spark SQL的优化器执行引擎的优点。Dataset可以使用JVM中的对象进行构造,然后使用转换函数(map, flatmap, filter等等)。Dataset API支持Scala和Java语言。Python中没有对Dataset API的支持。但是由于Python的动态特性,许多Dataset API中的优点已经可用了(i.e. 你可以通过row.columnName来访问一行数据中的一列)。R语言也和Python类似。
二、开始
2.1 入口:SparkSession
Spark中所有功能点的入口是SparkSession类。可以使用SparkSession.builder()方法来建立一个基础的SparkSession。
Find full example code at "examples/src/main/scala/org/apache/spark/examples/sql/SparkSQLExample.scala" in the Spark repo.
Spark2.0中的SarkSession提供了内置的Hive特性支持,包括使用HiveQL编写查询,访问Hive UDF和读取Hive tables中的数据。通过这些特性,用户不再需要有线程的Hive设置。
2.2 创建DataFrames
在SparkSession中,用户可以通过已存在的RDD,或是Hive table,或是Spark数据源来创建DataFrames。
下例展示了通过JSON文件来创建DataFrame的过程。
2.3 无类型的Dataset操作(又名DataFrame操作)
DataFrame提供了可以通过Scala,Java,Python和R调用的领域特定语言。
如上面所说,在Spark2.0中,DataFrame只是Scala和Java API中的Rows的Dataset。这些操作同样被称为“无类型的转换”,与之产生对比的是强类型的Scala/Java Datasets操作。
下面是一些基础的Dataset操作样例
Find full example code at "examples/src/main/scala/org/apache/spark/examples/sql/SparkSQLExample.scala" in the Spark repo.
Dataset的全部操作列表可以参考http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.Dataset
除了简单的列引用和表达式,Dataset还有非常丰富的函数库,包括字符串操作,日期计算,通用数学操作等等。完整的列表请参考http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.functions$
2.4 编程运行SQL查询
SparkSession中的sql函数允许应用编程运行SQL查询并以DataFrame格式返回结果。
2.5 全局临时视图
Spark SQL的临时视图作为范围为单个session如果创建它的session中止了,它也会一起消失。如果你希望在全部session中共享一个临时视图,并且该临时视图在Spark应用中止前一直保持可用状态,那么你可以创建一个全局临时视图。全局临时视图和系统预留的数据库global_temp关联起来,并且我们必须使用这个限定名去引用它。例如,SELECT * FROM global_temp.view1
.
2.6 创建Dataset
Dataset和RDD类似,然而,Dataset使用了一种特别的编码来序列化以实现网络传输,而不是使用Java serialization或是Kryo序列化协议。编码协议和标准序列化协议都可以将对象转换为字节码,但是编码器是自动生成的代码并且使用了一种格式来允许Spark不用讲字节码反序列化为对象的情况下执行许多类似过滤、排序和hash操作。
2.7 与RDD的交互
Spark SQL支持通过两种不同的方法来将RDD转换为Dataset。第一种方法通过反射来推断包含指定对象类型的RDD的Schema。这种基于反射的方法可以让代码更加简洁,并且在你编写Spark应用时已经知道schema时会非常有效。
第二种创建Dataset的方法是一种编程接口,它能允许你去创建一个Schema并应用到已有的RDD上。虽然这种方法比较冗长,但它可以支持只有在运行时才能获取列的类型情况下去创建Dataset。
2.7.1 反射推断Schema
Scala的Spark SQL接口支持自动将包含case class的RDD转换成DataFrame。case class定义了table的schema。case class中的参数名称会被反射读取成为table中的列名。case class同样可以嵌套或是包含一些复杂类型,例如Seq或是Array。RDD可以被隐式转换为DataFrame并注册成为一个table。注册后的table可以用于随后的SQL语句。
2.7.2 编程指定Schema
当case class没有被提前定义时(例如,记录的结构被编码成了一个string,或是一个text dataset会被解析并且其中的列会根据不同的用户来进行投影),DataFrame可以通过三个步骤来在程序中创建。
- 从原始RDD创建一个Row的RDD
- 创建一个由StructType代表的schema来适配Step1中创建的RDD的row结构
- 通过SparkSession提供的createDataFrame将schema应用到Row RDD
例如:
2.8 聚合
http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.functions$
DataFrame的内置函数提供了常用的聚合函数,比如count,countDistinct,avg,max,min等等。然而这些函数是为DataFrame设计的,Spark SQL在Scala和Java语言中同样也有类型安全的版本实现,来针对强类型的Dataset使用。此外,用户不会受限于这些预定义的聚合函数,可以进行自定义创建。
2.8.1 无类型的用户定义聚合函数
用户需要继承UserDefinedAggregateFunction抽象类来实现用户自定义的无类型聚合函数。例如,一个用户定义的average函数如下:
2.8.2 类型安全的用户定义聚合函数
用户针对强类型的Datasets定义的聚合操作是围绕着Aggregator抽象类进行的。例如,一个类型安全的用户定义average函数如下:
三、数据源
Spark SQL支持通过DataFrame接口在多种不同的数据源执行操作。一个DataFrame可以执行关系型的转换函数,同时也能被用来创建一个临时视图。将DataFrame注册成一个临时视图意味着你可以在这些数据上执行SQL查询。本节描述了Spark Data Sources的加载和保存数据的通用方法,然后对内置的数据源的特定操作进行详细介绍。
3.1 通用Load/Save函数
在最简单的格式上,默认的data source (parquet,除非通过spark.sql.source.default进行配置了)可以执行所有的操作。
3.1.1 手动指定选项
你还可以手动指定将被使用的数据源,以及你想要传递给数据源的任何额外选项。data source通过它们的类完全限定名称指定(例如,org.apache.spark.sql.parquet),但是对于内置的data source,你也可以使用它们的类型简写(json,parquet,jdbc,orc,libsvm,csv,text)。从任何数据源类型加载的DataFrames都可以使用该语法转换为其他类型。
3.1.2 在文件上运行SQL
你可以直接通过SQL语句执行查询,而不是通过API来加载文件到DataFrame上再执行查询。
3.1.3 保存Modes
保存可以手动指定保存模式SaveMode,它会指定以何种方式处理现有数据。认识到这些保存模式不使用任何锁定,也不是原子的是很重要的。另外,当执行Overwrite操作时,在写入新数据之前老数据会被删除。
3.1.4 保存为持久化Tables
DataFrame也可以通过saveAsTable命令来保存Hive metastore中的持久化table。注意,没有必要使用现有的Hive部署来使用这个特性。Spark会为你创建一个默认的本地Hive metastore(使用Derby)。和createOrReplaceTempView命令不同,saveAsTable会将DataFrame中的内容进行物化,并且创建一个指向Hive metastore中数据的一个引用。即使你的Spark程序重启了,持久化的table仍然会存在,只要你保持着对同一个metastore的连接。可以通过在SparkSession上以表的名称调用tale方法来创建持久表的DataFrame。
对于基于文件的data source,例如,text,parquet,json等等。你可以通过path选项指定自定义表的路径。例如,df.write.option ("path", "/some/path").saveAsTable("t")。当table被drop时,自定义表路径会被移除,但table中的数据仍然存在。如果没有指定自定义table的path。Spark会在仓库路径下将数据写入到默认table path。当table被drop时,默认的table path也会被移除。
从Spark 2.1开始,持久的datasource table会将分区元数据存入Hive metastore。这带来几个好处:
- 由于metastore可以针对查询的分区返回必要的分区,不在需求在首次查询时扫描table的全部分区数据。
- Hive DDL操作,例如ALTER TABLE PARTITION ... SELECT LOCATION现在支持由Datasource API创建的table
注意,在创建外部数据源表(具有指定路径的)时,不默认收集分区信息。你可以通过触发MSCK REPAIR TABLE来将分区数据同步到metastore。
3.1.5 分桶,排序和分区
对于基于文件的datasource来说,也支持在输出中进行分桶和排序或是分区操作。分桶和排序操作只适用于持久化的table
可以针对单个表同时使用分区和分桶。
partitionBy操作会创建一个目录结构,参考3.2.2 分区发现章节。因此,它对高基数列的实用性有限。相反,bucketBy将数据分布到固定数目的桶中,可以在无限多独特值条件下使用。
3.2 Parquet文件
Parquet http://parquet.io/ 是一种被其他很多数据处理系统所支持的列格式。Spark SQL提供了针对Parquet文件的读写操作,这样可以自动保存原始数据的模式。当写Parquet文件时,所有的列都会自动转换为支持控制的格式来进行适配。
3.2.1 程序加载数据
3.2.2 分区发现
表分区是一种很多系统例如Hive中常见的优化策略。在一个分区表中,数据通常会在不同的目录下存放,分区列的值会编码到每一个分区目录的路径中。Parquet数据源目前可以自动发现和推断分区信息。例如,我们可以通过下面的目录结构来以分区表的形式存储我们的样例人口数据,使用两个额外的列,gender和country作为分区列。
通过传递path/to/table到SparkSession.reqd.parquet或是SparkSession.read.load接口时,Spark SQL会自动从路径中提取出分区信息。现在返回的DataFrame的schema成为如下格式:
注意到分区列的数据类型是被自动推断出来。目前,数值和字符串的数据类型支持自动推断。有时候,用户可能不希望对于分区列的数据类型进行自动推断。针对这种情况,我们可以通过spark.sql.sources.partitionColumnTypeInference.enabled参数来设置自动推断的类型,默认值为true。当类型推断功能被禁用时,Spark会认为分区列的数据类型是字符串。
从Spark1.6.0开始,分区发现只能在默认的路径下发现分区。在上面的例子中,如果用户传递path/to/table/gender=male给SparkSession.read.parquet或是SparkSession.read.load,gender不会被认为是一个分区列。如果用户需要制定分区发现开始的基础路径,可以在data source选项中设置basePath参数。例如,当数据路径是path/to/table/gender=male并且用户设置basePath为path/to/table/,gender才会被当做分区列进行处理。
3.2.3 Schema合并
像ProtoclBuffer,Avro和Thrift一样,Parquet同样支持schema动态更新。用户可以由一个简单的schema开始,再根据需要逐渐增加更多列到schema中。以这种方式,用户可能会最终得到不同但有互相兼容的schema的多个Parquet文件。Parquet data source目前支持动态监测这种情况并且合并这些文件的schema。
由于schema合并是一种代价相对较高的操作,并且在大多数情况下是不必要的,我们从1.5.0开始将它默认设置为关闭。你可以通过以下方式开发:
- 在读取Parquet文件(如上面示例)时设置data source 中的mergeSchema选项为true
- 设置全局的SQL属性spark.sql.parquet.mergeSchema为true
3.2.4 Hive metastore Parquet table转换
在读写Hive metastore Parquet tables时,Spark SQL处于性能考虑会尝试使用它自己的Parquet支持而不是Hive SerDe。这种行为是由spark.sql.hive.convertMetastoreParquet配置项控制的,默认开启。
3.2.4.1 Hive/Parquet Schema融合
在处理table schema时,Hive和Parquet有两个关键区别:
- Hive不区分大小写,但Parquet相反
- Hive任务所有列都可为空,但在Parquet中,空值属性很重要
由于以上原因,我们在讲Hive metastore Parquet table转换为Spark SQL Parquet table时必须对Hive metastore schema和Parquet schema进行融合。融合规则如下:
- 有同样名字的Field在两种schema中必须有同样的数据类型,不考虑空值属性。融合Field必须有Parquet端的数据类型,因此,能保留其空值属性。
- 融合后的schema包含了Hive metastore中的schema
- 只在Parquet schema中出现的Field会在融合后的schema中被丢弃
- 只在Hive metastore schema中出现的Field在融合后的schema中会作为一个可以为空的Field出现
3.2.4.2 Metadata更新
Spark SQL会处于性能考虑将Parquet metadata进行缓存。当Hive metastore和Parquet table的转换被开启时,这些转换后的table的metadata也会被缓存。如果这些表被Hive或是外部工具更新,你需要手动刷新metadata来保证元数据一致性。
3.2.5 配置
Parquet的配置项可以通过使用SparkSession中的setConf方法或是在SQL中运行SET key=value命令来进行设置
3.3 JSON Datasets
Spark SQL 可以自动推断JSON dataset中的schema,并且将它作为一个Dataset[Row]加载进来。这个转换行为可以通过在Dataset[String]或是一个JSON文件上使用SparkSession.read.json()中进行。
注意以一个json file提供的文件不是一个典型的JSON文件(???)。每一行必须包含一个分隔符,独立有效的JSON对象。关于JSON文件的更多信息请参考http://jsonlines.org/
对于常规的多行JSON文件,可以将multiLine参数设置为true:
3.4 Hive Tables
Spark SQL同样支持对存储在Apache Hivehttp://hive.apache.org/中的数据进行读写。然而,由于Hive有大量的依赖,这些依赖在默认的Spark distribution中没有包含进去。如果Hive依赖在classpath中没有找到,Spark会自动加载它们。注意,这些Hive依赖必须存在于所有的工作节点上,因为为了访问在Hive中存储的数据,必须访问Hive的序列化和反序列化库(SerDes)。
通过在工程里的conf/目录下放置hive-site.xml,core-site.xml(用于安全性配置)和hdfs-site.xml(用于HDFS配置)可以完成对Hive的配置。
当使用Hive时,我们需要在实例化SparkSession时加上Hive支持,包括连接到一个持久化的Hive metastore,支持Hive serdes,和Hive UDF。用户可以在没有已存在Hive部署版本情况下依然开启Hive支持。当没有配置hive-site.xml时,Spark Context会自动在当前目录下创建一个metastore_db,并且根据spark.sql.warehouse.dir配置创建目录,该配置项将Spark application启动的当前目录作为spark-warehouse的默认值。请注意hive-site.xml配置项中hive.metastore.warehouse.dir属性在Spark 2.0.0中被废弃了。作为替代选项,我们可以使用spark.sql.warehouse.dir来指定warehouse的默认位置。你可能需要在Spark application启动时将写权限赋予给用户。
3.4.1 为Hive tables指定存储格式
当创建一个Hive table时,你需要指定这个table应该如何从文件系统中读写数据,例如,输入格式和输出格式。同样也需要定义table如何将数据反序列化为rows,或是如何将rows序列化为数据,例如,serde。下列配置项可以用来指定存储格式(serde,输入格式,输入格式),例如, CREATE TABLE src(id int) USING hive OPTIONS(fileFormat 'parquet')。默认情况下,我们将会以plain text格式读取table。请注意,在创建table时不支持使用Hive storage handler,你可以在Hive端创建table时指定存储格式,再以Spark SQL来读取数据。
所有以OPTIONS定义的其他属性将被视为Hive serde属性。
3.4.2 和不同版本的Hive Metastore交互
Spark SQL中对Hive的支持一个最重要的部分是和Hive metastore进行交互,它能允许Spark SQL去读取Hive table的metadata。从Spark 1.4.0开始,可以使用Spark SQL中的一个单独的二进制构建文件来查询不同版本的Hive metastore,使用的配置信息如下。请注意,Hive中独立的各版本都与metastore进行交互了,Spark SQL内部会编译到Hive 1.2.1(???)并且使用这些类文件来进行额外的扩展(serdes,UDFs,UDAFs,等等)。
下列配置项被用来配置Hive版本,用于读取metadata
3.5 JDBC连接其他数据库
Spark SQL同样支持使用JDBC来读取其他数据库的数据作为data source。这个功能应该优先使用JdbcRDDhttp://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.rdd.JdbcRDD。因为通过这种方式,数据将以DataFrame返回,可以很容易地使用Spark SQL进行处理或是和其他数据源的数据进行join。JDBC数据源也更容易从Java或Python中使用,因为它不需要用户提供ClassTag(???)(注意,这与Spark SQL JDBC服务器不同,它允许其他应用程序使用Spark SQL运行查询)。
为了使用这项功能,你需要在spark classpath中添加你指定的数据库的JDBC driver。例如,如果要从Spark Shell中访问postgres,你需要执行下列命令:
远程数据库中的table可以被当做一个DataFrame或是使用Data Sources API作为一个Spark SQL临时视图来进行加载。用户可以在data source配置项中指定JDBC 连接属性。user和password一般是在连接属性中提供来登录data sources。除了连接属性,Spark 同样支持下列的不区分大小写的配置项:
3.6 常见问题
- JDBC driver class必须要在client session和所有executor中的主类加载器中可见。这是因为Java的DriverManager类做了一个安全检查,它在新建一个连接时忽略了所有在主类加载器中的drivers。一个较为方便的方法是在所有的工作节点上修改compute_classpath.sh文件来包含你的driver JARs。
- 一些数据库,例如H2,将所有的名称转换成了大写,在Spark SQL中进行引用时需要使用大写进行引用。
四、性能调优
对某些情况下的工作负载来说,可以通过内存缓存或是开启一些实验性的选项来进行性能提升。
4.1 内存缓存
Spark SQL可以调用 spark.catalog.cacheTable("tableName")
或是dataFrame.cache()来使用列式存储格式缓存table数据。Spark SQL会只扫描所需要的列以及调整压缩策略来最小化内存使用和GC压力。你可以调用spark.catalog.uncacheTable("tableName")来从内存中移除table缓存。
内存缓存配置项可以通过调用SparkSession中的setConf方法或是使用SQL命令SET key=value来进行设置。
4.2 其他配置项
下列选项可以用来对查询性能进行调优。随着更多的优化自动执行,这些选项可能会在未来发布中被弃用。
五、分布式SQL引擎
Spark SQL同样可以通过使用它的JDBC/ODBC或是命令行接口来作为一个分布式的查询引擎使用。在这种模式下,终端用户或是application可以直接和Spark SQL进行交互来运行SQL查询,不需要编写任何代码。
5.1 运行Thrift JDBC/ODBC服务器
这里实现的Thrift JDBC / ODBC服务器对应于Hive 1.2.1中的HiveServer2,您可以使用Spark或Hive 1.2.1来测试JDBC服务器。
要启动JDBC / ODBC服务器,请在Spark目录中运行以下操作:
这个脚本可以接受所有的bin/spark-submit的命令行选项,加上--hiveconf的配置来指定Hive参数。你可以运行./sbin/start-thriftserver.sh --help
来获取完整的参数列表。默认情况下,这个服务会监听localhost:10000端口。你可以通过环境变量来覆盖这个行为
或是通过系统属性
现在你可以使用beeline来测试Thrift JDBC/ODBC server
连接到beeline中的JDBC / ODBC服务器:
Beeline会要求username和password。在非安全模式下可以直接输入机器的username和空白的password。在安全模式下,请参考https://cwiki.apache.org/confluence/display/Hive/HiveServer2+Clients
Hive的配置项可以使用conf/目录下的 hive-site.xml
, core-site.xml
和 hdfs-site.xml文件。
你还可以使用带有Hive的beeline脚本。
Thrift JDBC服务器也支持通过HTTP传输发送Thrift RPC消息。使用conf / xml文件的以下设置启用HTTP模式作为系统属性或在hive站点:
请使用beeline以http模式连接到JDBC / ODBC服务器进行测试
5.2 运行Spark SQL CLI
Spark SQL CLI是一个以本地模式运行Hive metastore service和以命令行输入的方式执行查询的方便的工作。注意Spark SQL CLI不会和Thrift JDBC server进行交互。
在Spark目录下执行下列命令即可启动Spark SQL CLI
六、迁移指南
略
七、引用
7.1 数据类型
Spark SQL和DataFrames支持下列数据类型
- 数值类型
- ByteType
- ShortType
- IntegerType
- LongType
- FloatType
- DoubleType
- DecimalType
- 字符串类型
- StringType
- 二进制类型
- BinaryType
- 日期类型
- TimestampType
- DateType
- 复合类型
- ArrayType
- MapType
- StructType
所有的Spark SQL支持的数据类型都在org.apache.spark.sql.types包中,可以通过下面的方式进行引用
7.2 NaN语义
在处理不完全符合标准浮点语义的浮点数或双类型时,需要特别处理not -a- number(NaN)。具体地说: