簡(jiǎn)介
sparkSQL 是為了讓開(kāi)發(fā)人員擺脫 自己編寫 RDD 原生代碼而產(chǎn)生的,只需要寫一句 SQL語(yǔ)句或者調(diào)用API,進(jìn)行查詢 或?qū)崿F(xiàn)更復(fù)雜的數(shù)據(jù)分析,使得開(kāi)發(fā)變得更簡(jiǎn)潔。
Spark SQL 允許開(kāi)發(fā)人員直接處理RDD,同時(shí)也可查詢Hive、HBase等外部數(shù)據(jù)源。
從Shark說(shuō)起
Shark即Hive on Spark,為了實(shí)現(xiàn)與Hive兼容,Shark重用了Hive中的HiveQL解析,以近似認(rèn)為僅從MapReduce作業(yè) 替換成了 Spark作業(yè),通過(guò)Hive的HiveQL解析,把HiveQL翻譯成Spark上的RDD操作。
這樣設(shè)計(jì)導(dǎo)致兩個(gè)問(wèn)題:
- 執(zhí)行計(jì)劃優(yōu)化完全依賴于Hive,不方便添加新的優(yōu)化策略
- Spark是線程級(jí)并行,而MapReduce是進(jìn)程級(jí)并行,因此,Spark在兼容Hive的實(shí)現(xiàn)上存在線程安全問(wèn)題。
因此,在2014年的時(shí)候,Shark項(xiàng)目中止,并轉(zhuǎn)向Spark SQL的開(kāi)發(fā)。
Spark SQL設(shè)計(jì)
Spark SQL增加了Schema RDD(即帶有Schema信息的RDD,即DataFrame),使用戶可以在Spark SQL中執(zhí)行SQL語(yǔ)句。
數(shù)據(jù)既可以來(lái)自RDD,也可以來(lái)自Hive、HDFS、Cassandra等外部數(shù)據(jù)源,也可以是json數(shù)據(jù)。
Spark SQL目前支持Scala、Java、Python三種語(yǔ)言,
Spark SQL可以很好地支持SQL查詢,
一方面,可以編寫Spark應(yīng)用程序使用SQL語(yǔ)句進(jìn)行數(shù)據(jù)查詢,
另一方面,也可以使用標(biāo)準(zhǔn)的數(shù)據(jù)庫(kù)連接器(比如JDBC)連接Spark進(jìn)行SQL查詢。
DataFrame
DataFrame 是什么?
DataFrame的推出,讓Spark具備了處理大規(guī)模結(jié)構(gòu)化數(shù)據(jù)的能力,
Spark能夠輕松實(shí)現(xiàn)從 MySQL 到 DataFrame的轉(zhuǎn)化,并且支持SQL查詢。
與RDD的區(qū)別:
RDD是分布式的 Java對(duì)象的數(shù)據(jù)集
RDD[Person]是以Person為類型參數(shù),但是,Person類的內(nèi)部結(jié)構(gòu)對(duì)于RDD而言卻是不可知的。
DataFrame是一種以RDD為基礎(chǔ)的分布式數(shù)據(jù)集,也就是分布式的Row對(duì)象的集合(每個(gè)Row對(duì)象代表一行記錄),提供了詳細(xì)的結(jié)構(gòu)信息,也就是我們經(jīng)常說(shuō)的模式(schema),
Spark SQL可以清楚地知道該數(shù)據(jù)集中包含哪些列、每列的名稱和類型,
可以方便的通過(guò) Spark SQL 進(jìn)行查詢
Schema是什么?
DataFrame中提供了詳細(xì)的數(shù)據(jù)結(jié)構(gòu)信息,從而使得SparkSQL可以清楚地知道該數(shù)據(jù)集中包含哪些列,每列的名稱和類型各是什么,
DataFrame中的數(shù)據(jù)結(jié)構(gòu)信息,即為schema。
DataFrame 的創(chuàng)建
從Spark2.0以上版本開(kāi)始,Spark使用全新的SparkSession接口替代Spark1.6中的SQLContext及HiveContext接口來(lái)實(shí)現(xiàn)其對(duì)數(shù)據(jù)加載、轉(zhuǎn)換、處理等功能。
SparkSession實(shí)現(xiàn)了 SQLContext 及 HiveContext 所有功能。
先創(chuàng)建一個(gè) SparkSession:
import org.apache.spark.sql.SparkSession
val spark = SparkSession.builder().getOrCreate()
然后從json文件中讀取
//使支持RDDs轉(zhuǎn)換為DataFrames及后續(xù)sql操作
import spark.implicits._
//后面用到的 spark 都是和上面的 spark 是同一個(gè)
val df = spark.read.json("file:///zyb/people.json")
json文件的內(nèi)容為:
{"name":"Michael"}
{"name":"Andy", "age":30}
{"name":"Justin", "age":19}
展示 Dataframe 數(shù)據(jù):
df.show()
//結(jié)果為:
+----+-------+
| age| name|
+----+-------+
|null|Michael|
| 30| Andy|
| 19| Justin|
+----+-------+
看一下 DataFrame 的 Schema
df.printSchema()
//結(jié)果為:
root
|-- age: long (nullable = true)
|-- name: string (nullable = true)
通過(guò) sql 語(yǔ)句進(jìn)行查詢
//必須注冊(cè)為臨時(shí)表才能供下面的查詢使用
df.registerTempTable("peopleTempTab")
spark.sql("select name,age from peopleTempTab where age > 20").rdd
常用的 DataFrame操作
// 選擇多列,并為 age 的列 ,都加1
df.select(df("name"),df("age")+1).show()
// 條件過(guò)濾
df.filter(df("age") > 20 ).show()
// 分組聚合
df.groupBy("age").count().show()
從 RDD 轉(zhuǎn)換為 DataFrame
這里介紹兩種轉(zhuǎn)換方式:
1、利用反射機(jī)制推斷RDD模式
這種方式要先確定類的成員組成,即Schema信息。
先定義一個(gè) case class
case class Person(name: String, age: Long)
把從文件中讀的字符串轉(zhuǎn)換為 Person,然后調(diào)用 case class 的 toDF() 方法
val peopleDF = spark.sparkContext.textFile("file:///zyb/people.txt").map(_.split(",")).map(att => Person(att(0), att(1))).toDF()
2、使用編程方式定義RDD模式
先 生成一個(gè) schema,然后生成一個(gè) Row,然后調(diào)用 spark的 createDataFrame 方法把兩者組裝起來(lái)。
生成一個(gè) RDD:
val studentRDD = spark.sparkContext.parallelize(Array("3 Rongcheng M 26","4 Guanhua M 27")).map(_.split(" "))
創(chuàng)建 schema:
val schema = StructType(List(StructField("id", IntegerType, true),StructField("name", StringType, true), StructField("gender", StringType, true),StructField("age", IntegerType, true)))
創(chuàng)建Row對(duì)象,每個(gè)Row對(duì)象都是rowRDD中的一行:
val rowRDD = studentRDD.map(p => Row(p(0).toInt, p(1).trim, p(2).trim, p(3).toInt))
建立起Row對(duì)象和模式之間的對(duì)應(yīng)關(guān)系,也就是把數(shù)據(jù)和模式對(duì)應(yīng)起來(lái)
val studentDF = spark.createDataFrame(rowRDD, schema)
下面 往 MySql 寫數(shù)據(jù)時(shí),也會(huì)用到這個(gè) studentDF
相比較而言,還是第一種方式簡(jiǎn)單。
DataFrame 的兩種保存方式
從 json文件中加載數(shù)據(jù)得到 DataFrame,load方式:
val peopleDF = spark.read.format("json").load("file:///usr/local/spark/examples/src/main/resources/people.json")
或者 手動(dòng)指定 格式:
val peopleDF = spark.read.json("file:///usr/local/spark/examples/src/main/resources/people.json")
- 通過(guò) DataFrame 的 write 方法
peopleDF.select("name", "age").write.format("csv").save("file:///usr/local/spark/mycode/newpeople.csv")
- 通過(guò) DataFrame 的 rdd方式
peopleDF.rdd.saveAsTextFile("file:///usr/local/spark/mycode/newpeople.txt")
DataFrame 讀寫 parquet
Parquet是一種流行的列式存儲(chǔ)格式,可以高效地存儲(chǔ)具有嵌套字段的記錄。
讀 parquet 文件,返回的為 DataFrame
spark.read.parquet("file:///zyb/users.parquet")
通過(guò) DataFrame 寫成 parquet
peopleDF.write.parquet("file:///usr/local/spark/mycode/newpeople.parquet")
Spark SQL 支持連接到 MySql Server
1、創(chuàng)建一個(gè)表
//創(chuàng)建一個(gè)數(shù)據(jù)庫(kù) spark
create database spark;
//使用數(shù)據(jù)庫(kù)
use spark;
//創(chuàng)建一個(gè)表
create table student (id int(4), name char(20), gender char(4), age int(4));
//往表里插入數(shù)據(jù)
insert into student values(1,"Xueqian",'F',23);
insert into student values(2,"wegfw",'M',24);
//從表中查詢
select * from student
2、通過(guò) jdbc 從MySql 中讀數(shù)據(jù)
下載 jdbc 驅(qū)動(dòng)
mysql-connector-java-5.1.40.tar.gz
把 驅(qū)動(dòng) 放到 spark的 jars 目錄下
啟動(dòng) spark-shell,要附加一些參數(shù),告訴jar的位置
./bin/spark-shell \ --jars /usr/local/spark/jars/mysql-connector-java-5.1.40/mysql-connector-java-5.1.40-bin.jar \ --driver-class-path /usr/local/spark/jars/mysql-connector-java-5.1.40/mysql-connector-java-5.1.40-bin.jar
啟動(dòng) shell 后,執(zhí)行以下命令連接數(shù)據(jù)庫(kù),讀取數(shù)據(jù)
這里要告訴 MySql的 URL地址,drive是誰(shuí),表是哪個(gè),用戶和密碼。
val jdbcDF = spark.read.format("jdbc").option("url", "jdbc:mysql://localhost:3306/spark").option("driver","com.mysql.jdbc.Driver").option("dbtable", "student").option("user", "root").option("password", "hadoop").load()
顯示 DataFrame
jdbcDF.show()
//結(jié)果為:
+---+--------+------+---+
| id| name|gender|age|
+---+--------+------+---+
| 1| Xueqian| F| 23|
| 2|Weiliang| M| 24|
+---+--------+------+---+
3、通過(guò) jdbc 往MySql 中寫數(shù)據(jù)
創(chuàng)建一個(gè) prop變量 用來(lái)保存 JDBC連接參數(shù)
val prop = new Properties()
prop.put("user", "root") //表示用戶名是root
prop.put("password", "hadoop") //表示密碼是hadoop
prop.put("driver","com.mysql.jdbc.Driver") //表示驅(qū)動(dòng)程序是com.mysql.jdbc.Driver
連接數(shù)據(jù)庫(kù),采用append模式,表示追加記錄到數(shù)據(jù)庫(kù)spark的student表中
studentDF.write.mode("append").jdbc("jdbc:mysql://localhost:3306/spark", "spark.student", prop)
Spark SQL 支持連接到 Hive
編譯Spark 源碼,使Spark 支持Hive
Spark官方提供的版本,通常是不包含Hive支持的,需要采用源碼編譯,編譯得到一個(gè)包含Hive支持的Spark版本。
編譯 支持 Hive 的 Spark 源碼步驟:
- 先到這個(gè)網(wǎng)址:http://spark.apache.org/downloads.html 下載源碼
- 傳到Linux,解壓:tar -zxvf ./spark-2.1.0.tgz -C /home/hadoop/
- 查看 Hadoop版本,下面要用到。查看命令:hadoop version
- 編譯
cd /home/hadoop/spark-2.1.0 ./dev/make-distribution.sh —tgz —name h27hive -Pyarn -Phadoop-2.7 -Dhadoop.version=2.7.1 -Phive -Phive-thriftserver -DskipTests
- -Phadoop-2.7 -Dhadoop.version=2.7.1
指定安裝 spark 時(shí)的 hadoop 版本,要和電腦上安裝的Hadoop的版本對(duì)應(yīng)。 - -Phive -Phive-thriftserver
這兩個(gè)選項(xiàng)讓其支持Hive。 - -DskipTests
能避免測(cè)試不通過(guò)時(shí)發(fā)生的錯(cuò)誤。 - h27hive
只是我們給編譯以后的文件的一個(gè)名稱,最終編譯成功后會(huì)得到文件名“spark-2.1.0-bin-h27hive.tgz”
- -Phadoop-2.7 -Dhadoop.version=2.7.1
編譯要花幾個(gè)小時(shí),而且可能還會(huì)遇到各種錯(cuò)誤,建議直接在網(wǎng)上下載一個(gè)編譯好的 Spark,提供一個(gè)下載地址:https://pan.baidu.com/s/1nv8Y2hj
配置
-
用vim編輯器打開(kāi)了spark-env.sh文件,在這個(gè)文件增加一行內(nèi)容,重復(fù)的就不需要添加:
export SPARK_DIST_CLASSPATH=$(/usr/local/hadoop/bin/hadoop classpath) export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 export CLASSPATH=$CLASSPATH:/usr/local/hive/lib export SCALA_HOME=/usr/local/scala export HADOOP_CONF_DIR=/usr/local/hadoop/etc/hadoop export HIVE_CONF_DIR=/usr/local/hive/conf export SPARK_CLASSPATH=$SPARK_CLASSPATH:/usr/local/hive/lib/mysql-connector-java-5.1.40-bin.jar
-
為了讓Spark能夠訪問(wèn)Hive,需要把Hive的配置文件hive-site.xml拷貝到Spark的conf目錄下:
cd /usr/local/sparkwithhive/conf cp /usr/local/hive/conf/hive-site.xml .
vim spark-env.sh
創(chuàng)建 Hive 表
我們進(jìn)入Hive:
cd /usr/local/hive
./bin/hive
新建一個(gè)數(shù)據(jù)庫(kù)sparktest,并在這個(gè)數(shù)據(jù)庫(kù)下面創(chuàng)建一個(gè)表student,并錄入兩條數(shù)據(jù)。
hive> create database if not exists sparktest;//創(chuàng)建數(shù)據(jù)庫(kù)sparktest
hive> show databases; //顯示一下是否創(chuàng)建出了sparktest數(shù)據(jù)庫(kù)
//下面在數(shù)據(jù)庫(kù)sparktest中創(chuàng)建一個(gè)表student
hive> create table if not exists sparktest.student(
> id int,
> name string,
> gender string,
> age int);
hive> use sparktest; //切換到sparktest
hive> show tables; //顯示sparktest數(shù)據(jù)庫(kù)下面有哪些表
hive> insert into student values(1,'Xueqian','F',23); //插入一條記錄
hive> insert into student values(2,'Weiliang','M',24); //再插入一條記錄
hive> select * from student; //顯示student表中的記錄
讀取 Hive 中的數(shù)據(jù)
Scala> import org.apache.spark.sql.Row
Scala> import org.apache.spark.sql.SparkSession
Scala> case class Record(key: Int, value: String)
// warehouseLocation points to the default location for managed databases and tables
Scala> val warehouseLocation = "spark-warehouse" //"file:${system:user.dir}/spark-warehouse"
Scala> val spark = SparkSession.builder().appName("Spark Hive Example"). \
config("spark.sql.warehouse.dir", warehouseLocation).enableHiveSupport().getOrCreate()
Scala> import spark.implicits._
Scala> import spark.sql
//下面是運(yùn)行結(jié)果
scala> sql("SELECT * FROM sparktest.student").show()
+---+--------+------+---+
| id| name|gender|age|
+---+--------+------+---+
| 1| Xueqian| F| 23|
| 2|Weiliang| M| 24|
+---+--------+------+---+
向 Hive 中插入數(shù)據(jù)
先創(chuàng)建一個(gè) DataFrame 數(shù)據(jù):
scala> import java.util.Properties
scala> import org.apache.spark.sql.types._
scala> import org.apache.spark.sql.Row
//下面我們?cè)O(shè)置兩條數(shù)據(jù)表示兩個(gè)學(xué)生信息
scala> val studentRDD = spark.sparkContext.parallelize(Array("3 Rongcheng M 26","4 Guanhua M 27")).map(_.split(" "))
//下面要設(shè)置模式信息
scala> val schema = StructType(List(StructField("id", IntegerType, true),StructField("name", StringType, true),StructField("gender", StringType, true),StructField("age", IntegerType, true)))
//下面創(chuàng)建Row對(duì)象,每個(gè)Row對(duì)象都是rowRDD中的一行
scala> val rowRDD = studentRDD.map(p => Row(p(0).toInt, p(1).trim, p(2).trim, p(3).toInt))
//建立起Row對(duì)象和模式之間的對(duì)應(yīng)關(guān)系,也就是把數(shù)據(jù)和模式對(duì)應(yīng)起來(lái)
scala> val studentDF = spark.createDataFrame(rowRDD, schema)
往 Hive 中插入數(shù)據(jù):
scala> studentDF.show()
+---+---------+------+---+
| id| name|gender|age|
+---+---------+------+---+
| 3|Rongcheng| M| 26|
| 4| Guanhua| M| 27|
+---+---------+------+---+
//下面注冊(cè)臨時(shí)表:tempTable
scala> studentDF.registerTempTable("tempTable")
//往 Hive 表:sparktest.student 中插入數(shù)據(jù)
scala> sql("insert into sparktest.student select * from tempTable")
對(duì) SQL 進(jìn)行查詢優(yōu)化
主要是對(duì)要查詢的 存儲(chǔ)的數(shù)據(jù)格式 進(jìn)行優(yōu)化,盡可能的使用列式格式 存儲(chǔ)數(shù)據(jù)。
什么是列存儲(chǔ)呢?
傳統(tǒng)的數(shù)據(jù)庫(kù)通常以行單位做數(shù)據(jù)存儲(chǔ),而列式存儲(chǔ)(后文均以列存儲(chǔ)簡(jiǎn)稱)以列為單位做數(shù)據(jù)存儲(chǔ),如下:
列存儲(chǔ)相比于行存儲(chǔ)主要有以下幾個(gè)優(yōu)勢(shì):
- 數(shù)據(jù)即索引,查詢是可以跳過(guò)不符合條件的數(shù)據(jù),只讀取需要的數(shù)據(jù),降低 IO 數(shù)據(jù)量(行存儲(chǔ)沒(méi)有索引查詢時(shí)造成大量 IO,建立索引和物化視圖代價(jià)較大)
- 只讀取需要的列,進(jìn)一步降低 IO 數(shù)據(jù)量,加速掃描性能(行存儲(chǔ)會(huì)掃描所有列)
- 由于同一列的數(shù)據(jù)類型是一樣的,可以使用高效的壓縮編碼來(lái)節(jié)約存儲(chǔ)空間
當(dāng)然列存儲(chǔ)并不是在所有場(chǎng)景都強(qiáng)于行存儲(chǔ),當(dāng)查詢要讀取多個(gè)列時(shí),行存儲(chǔ)一次就能讀取多列,而列存儲(chǔ)需要讀取多次。Spark 原始支持 parquet 和 orc 兩個(gè)列存儲(chǔ),下文的實(shí)踐使用 parquet
使用 Parquet 加速 Spark SQL 查詢
使用 Parquet 格式的列存儲(chǔ)主要帶來(lái)三個(gè)好處:
- 大大節(jié)省存儲(chǔ)空間
使用行存儲(chǔ)占用 44G,將行存儲(chǔ)轉(zhuǎn)成 parquet 后僅占用 5.6G,節(jié)省了 87.2% 空間,使用 Spark 將數(shù)據(jù)轉(zhuǎn)成列存儲(chǔ)耗時(shí)4分鐘左右(該值與使用資源相關(guān)) - 只讀取指定行
select count(distinct f1) from tbInRow/tbInParquet
行存儲(chǔ)耗時(shí): 119.7s
列存儲(chǔ)耗時(shí): 3.4s
加速 35 倍 - 跳過(guò)不符合條件數(shù)據(jù)
select count(f1) from tbInRow/tbInParquet where f1 > 10000
行存儲(chǔ)耗時(shí): 102.8s
列存儲(chǔ)耗時(shí): 1.3s
加速 78 倍