Spark SQL是Spark生态系统中非常重要的组件,其前身为Shark。Shark是Spark上的数据仓库,最初设计成与Hive兼容,但是该项目于2014年开始停止开发,转向Spark SQL。Spark SQL全面继承了Shark,并进行了优化。
Shark即Hive on Spark,为了实现与Hive兼容,Shark在HiveQL方面重用了Hive中的HiveQL解析、逻辑执行计划翻译、执行计划优化等逻辑,可以近似认为仅将物理执行计划从MapReduce作业替换成了Spark作业,通过Hive的HiveQL解析,把HiveQL翻译成Spark上的RDD操作。
Shark的设计导致了两个问题:
一是执行计划优化完全依赖于Hive,不方便添加新的优化策略;
二是因为Spark是线程级并行,而MapReduce是进程级并行,因此,Spark在兼容Hive的实现上存在线程安全问题,导致Shark不得不使用另外一套独立维护的打了补丁的Hive源码分支。
Shark的实现继承了大量的Hive代码,因而给优化和维护带来了大量的麻烦,特别是基于MapReduce设计的部分,成为整个项目的瓶颈。因此,在2014年的时候,Shark项目中止,并转向Spark SQL的开发。
Spark SQL在Hive兼容层面仅依赖HiveQL解析和Hive元数据,也就是说,从HQL被解析成抽象语法树(AST)起,就全部由Spark SQL接管了。Spark SQL执行计划生成和优化都由Catalyst(函数式关系查询优化框架)负责。
SparkSQL支持多种不同的外部数据源,将其转化为DataFrame,然后进行SQL查询。Spark SQL目前支持Scala、Java、Python三种语言,支持SQL-92规范。
1.DataFrame概述
DataFrame的推出,让Spark具备了处理大规模结构化数据的能力,不仅比原有的RDD转化方式更加简单易用,而且获得了更高的计算性能。Spark能够轻松实现从MySQL到DataFrame的转化,并且支持SQL查询。
DataFrame与RDD的区别
RDD是分布式的 Java对象的集合,比如,RDD[Person]是以Person为类型参数,但是,Person类的内部结构对于RDD而言却是不可知的。DataFrame是一种以RDD为基础的分布式数据集,也就是分布式的Row对象的集合(每个Row对象代表一行记录),提供了详细的结构信息,也就是我们经常说的模式(schema),Spark SQL可以清楚地知道该数据集中包含哪些列、每列的名称和类型。
和RDD一样,DataFrame的各种变换操作也采用惰性机制,只是记录了各种转换的逻辑转换路线图(是一个DAG图),不会发生真正的计算,这个DAG图相当于一个逻辑查询计划,最终,会被翻译成物理查询计划,生成RDD DAG,按照之前介绍的RDD DAG的执行方式去完成最终的计算得到结果。
2.dataframe的读取
从Spark2.0以上版本开始,Spark使用全新的SparkSession接口替代Spark1.6中的SQLContext及HiveContext接口来实现其对数据加载、转换、处理等功能。SparkSession实现了SQLContext及HiveContext所有功能。
SparkSession支持从不同的数据源加载数据,并把数据转换成DataFrame,并且支持把DataFrame转换成SQLContext自身中的表,然后使用SQL语句来操作数据。SparkSession亦提供了HiveQL以及其他依赖于Hive的功能的支持。
下面介绍如何使用SparkSession来创建DataFrame。
下面三条语句主要用来生成一个sparkSession对象,这个SparkSession对象就是整个sparksql的指挥官。这指挥官在独立应用程序时需要这三条语句生成的,但是在pyspark里,就不需要人工生成,因为启动pyspark可以自动生成。
>>> from pyspark import SparkContext,SparkConf
>>> from pyspark.sql import SparkSession
>>> spark = SparkSession.builder.config(conf=SparkConf()).getOrCreate()
首先,找到样例数据。 Spark已经为我们提供了几个样例数据,就保存在“/usr/local/spark/examples/src/main/resources/”这个目录下,这个目录下有两个样例数据people.json和people.txt。
hadoop@rachel-virtual-machine:/usr/local/spark/examples/src/main/resources$ ll
总用量 48
drwxr-xr-x 2 hadoop rachel 4096 2月 5 2019 ./
drwxr-xr-x 7 hadoop rachel 4096 2月 5 2019 ../
-rw-r--r-- 1 hadoop rachel 130 2月 5 2019 employees.json
-rw-r--r-- 1 hadoop rachel 240 2月 5 2019 full_user.avsc
-rw-r--r-- 1 hadoop rachel 5812 2月 5 2019 kv1.txt
-rw-r--r-- 1 hadoop rachel 49 2月 5 2019 people.csv
-rw-r--r-- 1 hadoop rachel 73 2月 5 2019 people.json
-rw-r--r-- 1 hadoop rachel 32 2月 5 2019 people.txt
-rw-r--r-- 1 hadoop rachel 185 2月 5 2019 user.avsc
-rw-r--r-- 1 hadoop rachel 334 2月 5 2019 users.avro
-rw-r--r-- 1 hadoop rachel 615 2月 5 2019 users.parquet
hadoop@rachel-virtual-machine:/usr/local/spark/examples/src/main/resources$ cat people.json
{"name":"Michael"}
{"name":"Andy", "age":30}
{"name":"Justin", "age":19}
hadoop@rachel-virtual-machine:/usr/local/spark/examples/src/main/resources$ cat people.txt
Michael, 29
Andy, 30
Justin, 19
hadoop@rachel-virtual-machine:/usr/local/spark/examples/src/main/resources$
spark可通过以下方式读取文件:
DataFrame读取json数据:
>>> df = spark.read.json("file:///usr/local/spark/examples/src/main/resources/people.json")
>>> df.show()
+----+-------+
| age| name|
+----+-------+
|null|Michael|
| 30| Andy|
| 19| Justin|
+----+-------+
3. DataFrame的保存
如何把RDD保存成文本文件:
注:这里需要给出全程路径
也可以使用下面的格式进行保存
从示例文件people.json中创建DataFrame,保存到另外一个json文件中:
>>> df.select("name","age").write.format("json").save("file:///usr/local/spark/mycode/sparksql/newpeople.json")
>>> df.select("name").write.format("text").save("file:///usr/local/spark/mycode/sparksql/newpeople.txt")
可以看出,这里使用select(“name”, “age”)确定要把哪些列进行保存,然后调用write.format(“json”).save ()保存成json文件。
write.format()支持输出 json,parquet, jdbc, orc, libsvm, csv, text等格式文件,如果要输出文本文件,可以采用write.format(“text”),但是,需要注意,只有select()中只存在一个列时,才允许保存成文本文件,如果存在两个列,比如select(“name”, “age”),就不能保存成文本文件。
在Shell命令提示符下查看新生成的newpeople.json、newpeople.txt:
hadoop@rachel-virtual-machine:/usr/local/spark/mycode/sparksql$ ll
总用量 16
drwxrwxr-x 4 hadoop hadoop 4096 8月 29 10:52 ./
drwxrwxr-x 4 hadoop hadoop 4096 8月 29 10:50 ../
drwxrwxr-x 2 hadoop hadoop 4096 8月 29 10:50 newpeople.json/
drwxrwxr-x 2 hadoop hadoop 4096 8月 29 10:52 newpeople.txt/
hadoop@rachel-virtual-machine:/usr/local/spark/mycode/sparksql$ cd newpeople.json
hadoop@rachel-virtual-machine:/usr/local/spark/mycode/sparksql/newpeople.json$ ll
总用量 20
drwxrwxr-x 2 hadoop hadoop 4096 8月 29 10:50 ./
drwxrwxr-x 4 hadoop hadoop 4096 8月 29 10:52 ../
-rw-r--r-- 1 hadoop hadoop 71 8月 29 10:50 part-00000-ff096ad7-a016-49cc-9d68-2ff83ed1336a-c000.json
-rw-r--r-- 1 hadoop hadoop 12 8月 29 10:50 .part-00000-ff096ad7-a016-49cc-9d68-2ff83ed1336a-c000.json.crc
-rw-r--r-- 1 hadoop hadoop 0 8月 29 10:50 _SUCCESS
-rw-r--r-- 1 hadoop hadoop 8 8月 29 10:50 ._SUCCESS.crc
hadoop@rachel-virtual-machine:/usr/local/spark/mycode/sparksql/newpeople.json$ cat part-00000-ff096ad7-a016-49cc-9d68-2ff83ed1336a-c000.json
{"name":"Michael"}
{"name":"Andy","age":30}
{"name":"Justin","age":19}
hadoop@rachel-virtual-machine:/usr/local/spark/mycode/sparksql/newpeople.json$
从上可以看出生成了一个newpeople.json目录和一个newpeople.txt目录。
注:newpeople.json是一个目录,不是文件。而下面的part-00000-ff096ad7-a016-49cc-9d68-2ff83ed1336a-c000.json才是我们存取的文件。
如果我们要再次把newpeople.json中的数据加载到RDD中,可以直接使用newpeople.json目录名称,而不需要使用part-00000-ff096ad7-a016-49cc-9d68-2ff83ed1336a-c000.json 文件,如下:
>>> textFile = sc.textFile("file:///usr/local/spark/mycode/sparksql/newpeople.json")
>>> textFile.foreach(print)
{"name":"Justin","age":19}
{"name":"Michael"}
{"name":"Andy","age":30}
4.Dataframe的常用操作
>>> df.printSchema() #打印dataframe的模式信息
root
|-- age: long (nullable = true)
|-- name: string (nullable = true)
#显示某列
>>> df.select(df["name"],df["age"]+1).show()
+-------+---------+
| name|(age + 1)|
+-------+---------+
|Michael| null|
| Andy| 31|
| Justin| 20|
+-------+---------+
>>> df.filter(df["age"]>20).show()#筛选大于20的用户
+---+----+
|age|name|
+---+----+
| 30|Andy|
+---+----+
>>> df.groupBy("age").count().show()#统计每个年龄出现的次数
+----+-----+
| age|count|
+----+-----+
| 19| 1|
|null| 1|
| 30| 1|
+----+-----+
>>> df.sort(df["age"].desc()).show() #根据age列进行降序排序
+----+-------+
| age| name|
+----+-------+
| 30| Andy|
| 19| Justin|
|null|Michael|
+----+-------+
>>> df.sort(df["age"].desc(),df["name"].asc()).show()#选择两列进行排序
+----+-------+
| age| name|
+----+-------+
| 30| Andy|
| 19| Justin|
|null|Michael|
+----+-------+
5.利用反射机制推断RDD模式
如何从RDD转换得到DataFrame,因为很多时候数据是以RDD形式存在的。Spark提供了两种方法来实现从RDD转换得到DataFrame,第一种方法是,利用反射来推断包含特定类型对象的RDD的schema,适用对已知数据结构的RDD转换;第二种方法是,使用编程接口,构造一个schema并将其应用在已知的RDD上。
现以下面的数据进行分析:
>>> from pyspark.sql import Row #用Row对象封装数据
#生成 RDD
>>> people = spark.sparkContext.textFile("file:///usr/local/spark/examples/src/main/resources/people.txt").map(lambda line:line.split(",")).map(lambda p:Row(name=p[0],age=int(p[1])))
>>> schemaPeople = spark.createDataFrame(people)#将RDD转换为DataFrame,并命名为schemaPeople
在上面的代码中,.map(lambda line:line.split(",")) 作用是对读入的这个RDD中的每一行元素都进行解析。这行内容经过 .map(lambda line:line.split(",")) 操作后,就得到一个集合{Michael,29}。后面经过 .map(lambda p:Row(name=p[0],age=int(p[1]))) 操作时,这时的p就是这个集合{Michael,29},这时p[0]就是Micheael,p[1]就是29,map(lambda p :Row(p[0],p[1].strip())) 就会生成一个Row对象,这个对象里面包含了两个字段的值,这个Row对象就构成了rowRDD中的其中一个元素。因为people 有3行文本,所以,最终,rowRDD中会包含3个元素,每个元素都是org.apache.spark.sql.Row类型。实际上,Row对象只是对基本数据类型(比如整型或字符串)的数组的封装,本质就是一个定长的字段数组。
转化为DataFrame后,不能进行查询,必须注册为临时表才能供下面查询
>>> schemaPeople.createOrReplaceTempView("people")#括号里的people并不是上面读取的people,而是将临时表命名为people
然后就可基于这个临时表进行查询
>>> peosonsDF = spark.sql("select name,age from people where age>20")
查询的结果返回的是一个DataFrame。
下面将DataFrame转换为RDD:
>>> personsDFRDD = peosonsDF.rdd.map(lambda p:"Name:"+p.name+","+"Age:"+str(p.age))
>>> personsDFRDD.foreach(print)
Name:Michael,Age:29
Name:Andy,Age:30
上面peosonsDF.rdd就把DataFrame转换为RDD了,然后才执行RDD中方法map。
6.使用编程方式定义RDD模式
当我们提前知道数据的结构时,可以反射机制得到RDD模式,但当无法提前获知数据结构时,采用编程方式定义RDD模式。
使用createDataFrame(rdd, schema)编程方式定义RDD模式。
1)先生成表头
>>> from pyspark.sql.types import *
>>> from pyspark.sql import Row
# 定义一个模式字符串
>>> schemaString = "name age"
# 根据模式字符串生成模式
>>> fileds = [StructField(field_name,StringType(),True) for field_name in schemaString.split(" ")]
>>> schema = StructType(fileds)
#从上面信息可以看出,schema描述了模式信息,模式中包含name和age两个字段
fileds是一个列表,列表包含两个元素,每个元素是一个StructField对象,每个StructField对象都是来描述一个字段。
2)生成表中记录
>>> lines = spark.sparkContext.textFile("file:///usr/local/spark/examples/src/main/resources/people.txt")
>>> parts = lines.map(lambda x:x.split(","))
>>> people = parts.map(lambda p :Row(p[0],p[1].strip()))
在上面的代码中,lines.map(lambda x:x.split(",")) 作用是对lines这个RDD中的每一行元素都进行解析。这行内容经过 lines.map(lambda x:x.split(",")) 操作后,就得到一个集合{Michael,29}。后面经过 map(lambda p :Row(p[0],p[1].strip())) 操作时,这时的p就是这个集合{Michael,29},这时p[0]就是Micheael,p[1]就是29,map(lambda p :Row(p[0],p[1].strip())) 就会生成一个Row对象,这个对象里面包含了两个字段的值,这个Row对象就构成了rowRDD中的其中一个元素。因为lines 有3行文本,所以,最终,rowRDD中会包含3个元素,每个元素都是org.apache.spark.sql.Row类型。实际上,Row对象只是对基本数据类型(比如整型或字符串)的数组的封装,本质就是一个定长的字段数组。
3)下面进行拼接
>>> schemaPelple = spark.createDataFrame(people,schema)
这条语句就相当于建立了rowRDD数据集和模式之间的对应关系,从而我们就知道对于rowRDD的每行记录,第一个字段的名称是schema中的“name”,第二个字段的名称是schema中的“age”。
4)进行查询
必须将这个DataFrame注册成为一个临时表才能够进行查询
#必须注册为临时表才能供下面查询使用
>>> schemaPeople.createOrReplaceTempView("people")
>>> results = spark.sql("select name,age from people")
>>> results
DataFrame[name: string, age: bigint]
>>> results.show()
+-------+---+
| name|age|
+-------+---+
|Michael| 29|
| Andy| 30|
| Justin| 19|
+-------+---+
http://ifeve.com/spark-sql-dataframes/
http://spark.apache.org/docs/1.6.2/api/scala/index.html#org.apache.spark.sql.AnalysisException