本文是对Spark SQL基础知识的一个学习总结,包含如下几部分的内容:
- 概述
- SparkSession类
- DataFrame类
- Sql语句操作
- DataSet类
- 数据源
- 小结
预备知识:
1、Spark SQL是在Spark Core基础上的一个扩展库,如果需要了解spark的基础知识,可参考文档《spark学习笔记1-基础部分》。
2、Spark SQL支持类SQL操作,所以对关系数据以及其sql语句提前了解有助于加快对Spark SQL的API的熟悉。
一、概述
Spark SQL是Spark用来操作结构化数据的组件。通过Spark SQL,用户可以使用SQL或者Apache Hive版本的SQL方言(HQL)来查询数据。Spark SQL支持多种数据源类型,例如Hive表、Parquet以及JSON等。Spark SQL不仅为Spark提供了一个SQL接口,还支持开发者将SQL语句融入到Spark应用程序开发过程中,无论是使用Python、Java还是Scala,用户可以在单个的应用中同时进行SQL查询和复杂的数据分析。
Spark SQL是spark扩展库中使用最广泛的库,本文将针对其一些重要的API如何使用进行介绍。
本文涉及的案例,我们会在spark shell命令行下进行代码的编写和演示。
spark下载的版本的examples目录下,提供了一些其扩展功能(包括Spark SQL)的应用举例,含源代码和一些资源文件。为了方便,本文需要使用的一些资源文件会直接使用examples目录中提供的。
Spark SQL提供了scala,java,python,R多种编程语言的API,本文使用的是针对scala的API。其相关API位于 org.apache.spark.sql 包中,编写代码时需要先import相关的类和对象。
二、SparkSession类
Spark中所有功能的入口点都是SparkSession类。要创建基本的SparkSession,只需使用SparkSession.builder()。
我们先启动spark-shell,注意,为了便于对exampels下的资源文件的相对路径引用,我们控制台的当前目录不要使用bin目录,而是使用bin目录的上级目录。
下面我们在spark shell命令行下进行代码的编写和执行。
首先我们执行的代码如下:
scala> import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.SparkSession
scala> val spark = SparkSession.builder().appName("demo").getOrCreate()
spark: org.apache.spark.sql.SparkSession = org.apache.spark.sql.SparkSession@2087c939
上面代码我们首先将SparkSession类import进来,然后接连调用多个方法创建SparkSession对象。
三、DataFrame类
我们先看一个简单的创建DataFrame对象的方式,我们可以将列表对象转换为DataFrame对象。如下面例子:
scala> val df = Seq(1,2).toDF()
df: org.apache.spark.sql.DataFrame = [value: int]
scala> df.show()
+-----+
|value|
+-----+
| 1|
| 2|
+-----+
上面代码先创建了一个整型列表,然后调用其toDF方法转换为DataFrame对象。并调用DataFrame的show方法显示数据。可以看出,对于这种基本类型,DataFrame的列名默认为value。
我们再看一个稍微复杂的例子,如下面代码所示:
scala> case class Person(name:String,age:Int)
defined class Person
scala> val df = Seq(Person("jack",12),Person("tom",20)).toDF()
df: org.apache.spark.sql.DataFrame = [name: string, age: int]
scala> df.show()
+----+---+
|name|age|
+----+---+
|jack| 12|
| tom| 20|
+----+---+
上面例子中,将一个自定义的类的列表转换为DataFrame,可以看出,这时类中的字段名自动变为DataFrame的列名。
在实际的项目中,我们一般会从外部数据源(如文件)来创建DataFrame对象。这可以通过Spark SQL的SparkSession对象来完成。Spark SQL支持通过DataFrame接口对各种数据源进行操作,包括本地文件,hdfs上的文件,jdbc数据,hive表等。
为了简单,我们使用spark的安装目录下的examples目录下的josn文件为例进行说明。例子代码如下:
scala> val df = spark.read.json("examples/src/main/resources/people.json")
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]
上面代码获得了一个DataFrame对象,其引用的数据就是people.json文件中的内容,其包含的信息如下:
{"name":"Michael"}
{"name":"Andy", "age":30}
{"name":"Justin", "age":19}
因为DataFrame概念上就是一个二维表,带有schema信息。对照上面json文件的内容,从控制台输出的信息看出,json对象中的name和age属性就会被映射成DataFrame的两个字段,并且都是有类型的,name被设为string类型,age被设为bigint类型。
有了DataFrame对象,我们就可以使用其各种方法来对数据进行操作。下面介绍一些常见的方法:
1、show方法
show方法用于显示DataFrame中的所有数据,代码如:
scala> df.show()
+----+-------+
| age| name|
+----+-------+
|null|Michael|
| 30| Andy|
| 19| Justin|
+----+-------+
可以看出,DataFrame以表格的方式显示其中的数据,类似我们在关系数据库操作中执行select语句返回结果的形式。
DataFrame类还提供了很多的特定方法完成对数据的操作,下面继续说明。
2、select方法
DataFrame的select方法用于返回一个新的数据集,可以只包含指定的列的数据。
scala> df.select("name")
res2: org.apache.spark.sql.DataFrame = [name: string]
scala> df.select("name").show()
+-------+
| name|
+-------+
|Michael|
| Andy|
| Justin|
+-------+
其select方法,可以返回新的DataFrame对象,其中数据只包含指定的列。
select方法还提供了更强的功能,如下面例子:
scala> df.select($"name",$"age"+1)
res7: org.apache.spark.sql.DataFrame = [name: string, (age + 1): bigint]
scala> df.select($"name",$"age"+1).show()
+-------+---------+
| name|(age + 1)|
+-------+---------+
|Michael| null|
| Andy| 31|
| Justin| 20|
+-------+---------+
可以看出,select方法中,还可以对字段进行表达式操作,类似关系数据库sql语句的select语句中对字段的操作。
3、filter方法
我们还可以利用DataFrame提供的filter方法来实现类似关系数据库中select语句中的where子句功能。如下面例子:
scala> df.filter($"age" > 21)
res9: org.apache.spark.sql.Dataset[org.apache.spark.sql.Row] = [age: bigint, name: string]
scala> df.filter($"age" > 21).show()
+---+----+
|age|name|
+---+----+
| 30|Andy|
+---+----+
可以看出filter方法的参数就是一个条件表达式,满足条件的数据保留,返回一个新的DataFrame对象。
3、汇聚计算
类似关系数据库的select语句,可以使用group by等子句进行一些分组汇总计算,DataFrame对象也有类似功能,如下面例子:
scala> df.groupBy("age")
res11: org.apache.spark.sql.RelationalGroupedDataset = org.apache.spark.sql.RelationalGroupedDataset@1e73e9e2
scala> df.groupBy("age").count()
res12: org.apache.spark.sql.DataFrame = [age: bigint, count: bigint]
scala> df.groupBy("age").count().show()
+----+-----+
| age|count|
+----+-----+
| 19| 1|
|null| 1|
| 30| 1|
+----+-----+
上面输出信息可以看出,DataFrame类的groupBy方法返回的不是DataFrame对象,而是RelationalGroupedDataset类型对象,然后调用RelationalGroupedDataset类的count方法返回一个DataFrame对象。
上面只是列举了DataFrame类的一些简单的列引用和表达式之外,数据集还具有丰富的函数库,包括字符串操作,日期算术,常用数学运算等,这里不再详细介绍。
从上面的例子可以看出,几个常见的操作与关系数据库的select语句非常类似。而Spark SQL的强大之处是其可以直接通过编写类sql语句来访问数据。下面的章节来介绍。
四、Sql语句操作
Spark SQL提供了使用sql语句来对数据进行操作。我们先看一个例子代码:
scala> import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.SparkSession
scala> val spark = SparkSession.builder().appName("demo").getOrCreate()
spark: org.apache.spark.sql.SparkSession = org.apache.spark.sql.SparkSession@3c5bb37d
scala> val df = spark.read.json("examples/src/main/resources/people.json")
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]
scala> df.createOrReplaceTempView("people")
scala> val sqlDF = spark.sql("SELECT * FROM people")
sqlDF: org.apache.spark.sql.DataFrame = [age: bigint, name: string]
scala> sqlDF.show()
+----+-------+
| age| name|
+----+-------+
|null|Michael|
| 30| Andy|
| 19| Justin|
+----+-------+
在上面代码中,我们看到df.createOrReplaceTempView("people") 这个语句,该语句的含义是 将创建的DataFrame注册为SQL临时视图,这样下面就可以调用SparkSession对象的sql方法来执行select语句,该方法返回的是DataFrame对象。
这样我们就可以执行各种复杂的的select语句,如下面例子:
spark.sql("SELECT * FROM people where age>20").show()
spark.sql("SELECT count(*) FROM people").show()
五、DataSet类
DataSet是spark在2.0中替换RDD的数据模型,它比RDD执行效率更高。有关DataSet类的介绍我们在《spark学习笔记1-基础部分》文档中已经有介绍。下面主要介绍它和DataFrame的关系。
DataSet和DataFrame非常类似,它们可以相互转换,也有很多相同的操作方法,下面举例来说明。
我们先看下DataSet如何转换为DataFrame对象,如下面例子:
scala> val ds = Seq("hello","world").toDS()
ds: org.apache.spark.sql.Dataset[String] = [value: string]
scala> val df = ds.toDF()
df: org.apache.spark.sql.DataFrame = [value: string]
上面例子代码先创建了一个DataSet对象,然后调用DataSet类的toDF方法转换为DataFrame对象。
我们再看一个例子:
case class Person(name:String,age:Int)
scala> val ds = Seq(Person("jack",12)).toDS()
ds: org.apache.spark.sql.Dataset[Person] = [name: string, age: int]
scala> val df = ds.toDF()
df: org.apache.spark.sql.DataFrame = [name: string, age: int]
scala> df.select("name")
res7: org.apache.spark.sql.DataFrame = [name: string]
scala> ds.select("name")
res8: org.apache.spark.sql.DataFrame = [name: string]
从上面例子可以看出,DataSet和DataFrame都有select方法。
下面我们再来看下如何将DataFrame对象转为为DataSet对象,如下面例子:
scala> val df = Seq("hello","world").toDF()
df: org.apache.spark.sql.DataFrame = [value: string]
scala> val ds = df.as[String]
ds: org.apache.spark.sql.Dataset[String] = [value: string]
scala> ds.show()
+-----+
|value|
+-----+
|hello|
|world|
+-----+
从上面代码可以看出,调用DataFrame的as方法,并指定转换后的数据类型,则可以把DataFrame转换为DataSet
下面再看一个例子:
import org.apache.spark.sql.SparkSession
val spark = SparkSession.builder().appName("newdemo").getOrCreate()
val df = spark.read.json("examples/src/main/resources/people.json")
case class Person(name:String,age:Long)
val ds = df.as[Person]
上面代码可以看出,我们定义了个与所加载数据的格式匹配的类,来作为DataSet中元素的数据类型。
前面的学习我们们知道,可以通过DataFrame创建临时表,然后执行sql语句。通过DataSet也是可以的。如下面例子:
scala> case class Person(name:String,age:Long)
defined class Person
scala> val ds = Seq(Person("jack",20),Person("tom",23)).toDS()
ds: org.apache.spark.sql.Dataset[Person] = [name: string, age: bigint]
scala> ds.createOrReplaceTempView("user")
scala> val re = spark.sql("select * from user where age>20");
re: org.apache.spark.sql.DataFrame = [name: string, age: bigint]
scala> re.show()
+----+---+
|name|age|
+----+---+
| tom| 23|
+----+---+
可以看出,DataSet和DataFrame在使用时其实没有太多的差别。
六、数据源
Spark SQL支持通过DataFrame接口对各种数据源进行操作,不仅可以从各种格式的数据源中获取数据,也可以将计算后的数据保存到各种格式中。本节介绍Spark SQL支持的常见数据源操作。
(一)默认数据源
Spark SQL默认的数据源格式是parquet格式,如下面例子:
scala> import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.SparkSession
scala> val spark = SparkSession.builder().appName("demo").getOrCreate()
spark: org.apache.spark.sql.SparkSession = org.apache.spark.sql.SparkSession@3c5bb37d
scala> val df = spark.read.load("examples/src/main/resources/users.parquet")
df: org.apache.spark.sql.DataFrame = [name: string, favorite_color: string ... 1 more field]
scala> df.show()
+------+--------------+----------------+
| name|favorite_color|favorite_numbers|
+------+--------------+----------------+
|Alyssa| null| [3, 9, 15, 20]|
| Ben| red| []|
+------+--------------+----------------+
scala> df.select("name", "favorite_color")
res1: org.apache.spark.sql.DataFrame = [name: string, favorite_color: string]
上面代码中我们看到有这个语句
val df = spark.read.load("examples/src/main/resources/users.parquet")
对比上面章节中的代码
val df = spark.read.json("examples/src/main/resources/people.json")
可以看出,后一语句通过函数名json指定加载的数据是json格式,而上一语句是个通用的load语句,这时默认的是parquet格式。
Parquet是面向分析型业务的列式存储格式,由Twitter和Cloudera合作开发,2015年5月从Apache的孵化器里毕业成为Apache顶级项目。Parquet仅仅是一种存储格式,它是语言、平台无关的,并且不需要和任何一种数据处理框架绑定,可适配多种语言和组件。更详细的关于Parquet格式,可查看相关资料。
我们可以将DataFrame对象中的数据保存到Parquet文件中,举例如下:
scala> df.select("name", "favorite_color").write.save("savedata")
上面代码先从df中获取一个新的数据集(DataFrame),然后调用write和save方法保存新的数据集中数据到本地。需要说明的是,最后在运行spark shell的当前目录生成的并不是save方法参数指定的 savedata 文件,而是生成了一个savedata目录,目录中含一些中间数据(类似Mapreduce计算生成的中间文件)和一个结果文件,文件名如part-00000-a3924506-d3e5-4129-aedc-4b744ce8af7b.snappy.parquet。
我们可以通过代码重新查看该parquet文件中数据,看看是否与预期的一致。代码如下:
scala> val newdf = spark.read.load("savedata/part-00000-a3924506-d3e5-4129-aedc-4b744ce8af7b.snappy.parquet")
newdf: org.apache.spark.sql.DataFrame = [name: string, favorite_color: string]
scala> newdf.show()
+------+--------------+
| name|favorite_color|
+------+--------------+
|Alyssa| null|
| Ben| red|
+------+--------------+
可以看出,该parquet文件中数据与前面的 df.select("name", "favorite_color")获取的数据一样。说明保存操作是成功的。
(二)指定数据源格式
我们在load数据时可以指定数据源的格式,如指定json格式:
val df = spark.read.format("json").load("examples/src/main/resources/people.json")
上面代码通过调用format函数指定加载数据的格式为json。
同样我们可以写入数据到文件,如:
df.select("name").write.save("peopledata")
上面代码会写入数据到parquet格式的文件中。如果我们希望写入还是json格式,可以按如下方式编写代码:
df.select("name").write.format("json").save("peoplejsondata")
需要说明的是,同前面介绍的一样,写入数据时save方法指定的参数是最后生成的目录名,具体的文件在目录中,除数据文件外,还包括一些中间过程文件。
我们还可以指定其它格式的文件,如csv等,这里不再一一介绍。
(三)利用Jdbc使用关系数据库
Spark SQL支持使用jdbc将关系数据库作为数据源,如果要在spark shell中使用jdbc连接数据库,需要事先加载相应数据库的驱动Jar包。
比如我们要连接mysql数据库,首先要有mysql的jdbc驱动库,然后使用这样的方式启动spark shell。
spark-shell --driver-class-path mysql-connector-java-5.1.5-bin.jar --jars mysql-connector-java-5.1.5-bin.jar
上面命令中的 mysql-connector-java-5.1.5-bin.jar 是连接Mysql数据库的jdbc驱动程序,这里没有指定路径,需要位于当前目录下。
启动spark-shell,然后就可以加载数据库中的表,代码如:
import java.util.Properties
val props = new Properties()
props.put("driver", "com.mysql.jdbc.Driver")
props.put("user", "root")
props.put("password", "")
val df = spark.read.jdbc("jdbc:mysql://localhost:3306/dpms", "test", props)
上述代码中spark.read.jdbc方法的第1个参数是连接mysql数据库的标准url信息,第2个参数是表名,第3个参数是连接数据库的基本信息。所需的信息与我们直接利用Java代码编写jdbc程序一样。
然后我们就可以正常使用上面创建的DataFrame对象了。
七、小结
在本文中,我们对如何使用Spark SQL的核心API进行数据处理进行了基本的介绍,并介绍了Spark SQL如何对接不同的数据源。本文没有涉及到Spark SQL的内部机制和原理。可以看出,站在使用的角度看,Spark SQL提供的对外接口比较人性化,使用起来很方便。