好久沒更新部落格了,之前學了一些 R 語言和機器學習的內容,做了一些筆記,之後也會放到部落格上面來給大家共享。一個月前就打算更新 Spark Sql 的內容了,因為一些別的事情耽誤了,今天就簡單寫點,Spark1.2 馬上就要出來了,不知道變動會不會很大,據說新增了很多的新功能呢,期待中...
首先宣告一下這個版本的程式碼是 1.1 的,之前講的都是 1.0 的。
Spark 支援兩種模式,一種是在 spark 裡面直接寫 sql,可以通過 sql 來查詢物件,類似. net 的 LINQ 一樣,另外一種支援 hive 的 HQL。不管是哪種方式,下面提到的步驟都會有,不同的是具體的執行過程。下面就說一下這個過程。
Sql 解析成 LogicPlan
使用 Idea 的快捷鍵 Ctrl + Shift + N 開啟 SQLQuerySuite 檔案,進行除錯吧。
def sql(sqlText: String): SchemaRDD = {
if (dialect == "sql") {
new SchemaRDD(this, parseSql(sqlText))
} else {
sys.error(s"Unsupported SQL dialect: $dialect")
}
}
複製程式碼
從這裡可以看出來,第一步是解析 sql,最後把它轉換成一個 SchemaRDD。點選進入 parseSql 函式,發現解析 Sql 的過程在 SqlParser 這個類裡面。 在 SqlParser 的 apply 方法裡面,我們可以看到 else 語句裡面的這段程式碼。
//對input進行解析,符合query的模式的就返回Success
phrase(query)(new lexical.Scanner(input)) match {
case Success(r, x) => r
case x => sys.error(x.toString)
}
複製程式碼
這裡我們主要關注 query 就可以。
protected lazy val query: Parser[LogicalPlan] = (
select * (
UNION ~ ALL ^^^ { (q1: LogicalPlan, q2: LogicalPlan) => Union(q1, q2) } |
INTERSECT ^^^ { (q1: LogicalPlan, q2: LogicalPlan) => Intersect(q1, q2) } |
EXCEPT ^^^ { (q1: LogicalPlan, q2: LogicalPlan) => Except(q1, q2)} |
UNION ~ opt(DISTINCT) ^^^ { (q1: LogicalPlan, q2: LogicalPlan) => Distinct(Union(q1, q2)) }
)
| insert | cache
)
複製程式碼
這裡面有很多看不懂的操作符,請到下面這個網址裡面去學習。這裡可以看出來它目前支援的 sql 語句只是 select 和 insert。
www.scala-lang.org/api/2.10.4/…
我們繼續檢視 select。
// ~>只保留右邊的模式 opt可選的 ~按順序合成 <~只保留左邊的
protected lazy val select: Parser[LogicalPlan] =
SELECT ~> opt(DISTINCT) ~ projections ~
opt(from) ~ opt(filter) ~
opt(grouping) ~
opt(having) ~
opt(orderBy) ~
opt(limit) <~ opt(";") ^^ {
case d ~ p ~ r ~ f ~ g ~ h ~ o ~ l =>
val base = r.getOrElse(NoRelation)
val withFilter = f.map(f => Filter(f, base)).getOrElse(base)
val withProjection =
g.map {g =>
Aggregate(assignAliases(g), assignAliases(p), withFilter)
}.getOrElse(Project(assignAliases(p), withFilter))
val withDistinct = d.map(_ => Distinct(withProjection)).getOrElse(withProjection)
val withHaving = h.map(h => Filter(h, withDistinct)).getOrElse(withDistinct)
val withOrder = o.map(o => Sort(o, withHaving)).getOrElse(withHaving)
val withLimit = l.map { l => Limit(l, withOrder) }.getOrElse(withOrder)
withLimit
}
複製程式碼
可以看得出來它對 sql 的解析是和我們常用的 sql 寫法是一致的,這裡面再深入下去還有遞迴,並不是看起來那麼好理解。這裡就不繼續講下去了,在解析 hive 的時候我會重點講一下,我認為目前大家使用得更多是仍然是來源於 hive 的資料集,畢竟 hive 那麼穩定。
到這裡我們可以知道第一步是通過 Parser 把 sql 解析成一個 LogicPlan。
LogicPlan 到 RDD 的轉換過程
好,下面我們回到剛才的程式碼,接著我們應該看 SchemaRDD。
override def compute(split: Partition, context: TaskContext): Iterator[Row] =
firstParent[Row].compute(split, context).map(_.copy())
override def getPartitions: Array[Partition] = firstParent[Row].partitions
override protected def getDependencies: Seq[Dependency[_]] =
List(new OneToOneDependency(queryExecution.toRdd))
複製程式碼
SchemaRDD 是一個 RDD 的話,那麼它最重要的 3 個屬性:compute 函式,分割槽,依賴全在這裡面,其它的函式我們就不看了。
挺奇怪的是,我們 new 出來的 RDD,怎麼會有依賴呢,這個 queryExecution 是啥,點選進去看看吧,程式碼跳轉到 SchemaRDD 繼承的 SchemaRDDLike 裡面。
lazy val queryExecution = sqlContext.executePlan(baseLogicalPlan)
protected[sql] def executePlan(plan: LogicalPlan): this.QueryExecution =
new this.QueryExecution { val logical = plan }
複製程式碼
把這兩段很短的程式碼都放一起了,executePlan 方法就是 new 了一個 QueryExecution 出來,那我們繼續看看 QueryExecution 這個類吧。
lazy val analyzed = ExtractPythonUdfs(analyzer(logical))
lazy val optimizedPlan = optimizer(analyzed)
lazy val sparkPlan = {
SparkPlan.currentContext.set(self)
planner(optimizedPlan).next()
}
// 在需要的時候加入Shuffle操作
lazy val executedPlan: SparkPlan = prepareForExecution(sparkPlan)
lazy val toRdd: RDD[Row] = executedPlan.execute()
複製程式碼
從這裡可以看出來 LogicPlan 是經過了 5 個步驟的轉換,要被 analyzer 和 optimizer 的處理,然後轉換成 SparkPlan,在執行之前還要被 prepareForExecution 處理一下,最後呼叫 execute 方法轉成 RDD.
下面我們分步講這些個東東到底是幹啥了。
首先我們看看 Anayzer,它是繼承自 RuleExecutor 的,這裡插句題外話,Spark sql 的作者 Michael Armbrust 在 2013 年的 Spark Submit 上介紹 Catalyst 的時候,就說到要從整體地去優化一個 sql 的執行是很困難的,所有設計成這種基於一個一個小規則的這種優化方式,既簡單又方便維護。
好,我們接下來看看 RuleExecutor 的 apply 方法。
def apply(plan: TreeType): TreeType = {
var curPlan = plan
//規則還分批次的,分批對plan進行處理
batches.foreach { batch =>
val batchStartPlan = curPlan
var iteration = 1
var lastPlan = curPlan
var continue = true
// Run until fix point (or the max number of iterations as specified in the strategy.
while (continue) {
//用batch種的小規則從左到右挨個對plan進行處理
curPlan = batch.rules.foldLeft(curPlan) {
case (plan, rule) =>
val result = rule(plan)
result
}
iteration += 1
//超過了規定的迭代次數就要退出的
if (iteration > batch.strategy.maxIterations) {
continue = false
}
//經過處理成功的plan是會發生改變的,如果和上一次處理接觸的plan一樣,這說明已經沒有優化空間了,可以結束,這個就是前面提到的Fixed point
if (curPlan.fastEquals(lastPlan)) {
continue = false
}
lastPlan = curPlan
}
}
curPlan
}
複製程式碼
看完了 RuleExecutor,我們繼續看 Analyzer,下面我只貼出來 batches 這塊的程式碼,剩下的要自己去看了哦。
val batches: Seq[Batch] = Seq(
//碰到繼承自MultiInstanceRelations介面的LogicPlan時,發現id以後重複的,就強制要求它們生成一個新的全域性唯一的id
//涉及到InMemoryRelation、LogicRegion、ParquetRelation、SparkLogicPlan
Batch("MultiInstanceRelations", Once,
NewRelationInstances),
//如果大小寫不敏感就把屬性都變成小寫
Batch("CaseInsensitiveAttributeReferences", Once,
(if (caseSensitive) Nil else LowercaseAttributeReferences :: Nil) : _*),
//這個牛逼啊,居然想迭代100次的。
Batch("Resolution", fixedPoint,
//解析從子節點的操作生成的屬性,一般是別名引起的,比如a.id
ResolveReferences ::
//通過catalog解析表名
ResolveRelations ::
//在select語言裡,order by的屬性往往在前面沒寫,查詢的時候也需要把這些欄位查出來,排序完畢之後再刪除
ResolveSortReferences ::
//前面講過了
NewRelationInstances ::
//清除被誤認為別名的屬性,比如sum(score) as a,其實它應該是sum(score)才對
//它被解析的時候解析成Project(Seq(Alias(g: Generator, _)),直接返回Generator就可以了
ImplicitGenerate ::
//處理語句中的*,比如select *, count(*)
StarExpansion ::
//解析函式
ResolveFunctions ::
//解析全域性的聚合函式,比如select sum(score) from table
GlobalAggregates ::
//解析having子句後面的聚合過濾條件,比如having sum(score) > 400
UnresolvedHavingClauseAttributes ::
//typeCoercionRules是hive的型別轉換規則
typeCoercionRules :_*),
//檢查所有節點的屬性是否都已經處理完畢了,如果還有沒解析出來的屬性,這裡就會報錯!
Batch("Check Analysis", Once,
CheckResolution),
//清除多餘的操作符,現在是Subquery和LowerCaseSchema,
//第一個是子查詢,第二個HiveContext查詢樹裡面把子節點全部轉換成小寫
Batch("AnalysisOperators", fixedPoint,
EliminateAnalysisOperators)
)
複製程式碼
可以看得出來 Analyzer 是把 Unresolved 的 LogicPlan 解析成 resolved 的,解析裡面的表名、欄位、函式、別名什麼的。
我們接著看 Optimizer, 從單詞上看它是用來做優化的,但是從程式碼上來看它更多的是為了過濾我們寫的一些垃圾語句,並沒有做什麼實際的優化。
object Optimizer extends RuleExecutor[LogicalPlan] {
val batches =
//遞迴合併相鄰的兩個limit
Batch("Combine Limits", FixedPoint(100),
CombineLimits) ::
Batch("ConstantFolding", FixedPoint(100),
//替換null值
NullPropagation,
//替換一些簡單的常量表示式,比如 1 in (1,2) 直接返回一個true就可以了
ConstantFolding,
//簡化like語句,避免全表掃描,目前支援'%demo%', '%demo','demo*','demo'
LikeSimplification,
//簡化過濾條件,比如true and score > 0 直接替換成score > 0
BooleanSimplification,
//簡化filter,比如where 1=1 或者where 1=2,前者直接去掉這個過濾,後者這個查詢就沒必要做了
SimplifyFilters,
//簡化轉換,比如兩個比較欄位的資料型別是一樣的,就不需要轉換了
SimplifyCasts,
//簡化大小寫轉換,比如Upper(Upper('a'))轉為認為是Upper('a')
SimplifyCaseConversionExpressions) ::
Batch("Filter Pushdown", FixedPoint(100),
//遞迴合併相鄰的兩個過濾條件
CombineFilters,
//把從表示式裡面的過濾替換成,先做過濾再取表示式,並且掉過濾裡面的別名屬性
//典型的例子 select * from (select a,b from table) where a=1
//替換成select * from (select a,b from table where a=1)
PushPredicateThroughProject,
//把join的on條件中可以在原表當中做過濾的先做過濾
//比如select a,b from x join y on x.id = y.id and x.a >0 and y.b >0
//這個語句可以改寫為 select a,b from x where x.a > 0 join (select * from y where y.b >0) on x.id = y.id
PushPredicateThroughJoin,
//去掉一些用不上的列
ColumnPruning) :: Nil
}
複製程式碼
真是用心良苦啊,看來我們寫 sql 的時候還是要注意一點的,你看人家花多大的功夫來優化我們的爛 sql。要是我肯定不優化。寫得爛就慢去吧!
接下來,就改看這一句了 planner(optimizedPlan).next() 我們先看看 SparkPlanner 吧。
protected[sql] class SparkPlanner extends SparkStrategies {
val sparkContext: SparkContext = self.sparkContext
val sqlContext: SQLContext = self
def codegenEnabled = self.codegenEnabled
def numPartitions = self.numShufflePartitions
//把LogicPlan轉換成實際的操作,具體操作類在org.apache.spark.sql.execution包下面
val strategies: Seq[Strategy] =
//把cache、set、expain命令轉化為實際的Command
CommandStrategy(self) ::
//把limit轉換成TakeOrdered操作
TakeOrdered ::
//名字有點蠱惑人,就是轉換聚合操作
HashAggregation ::
//left semi join只顯示連線條件成立的時候連線左邊的表的資訊
//比如select * from table1 left semi join table2 on(table1.student_no=table2.student_no);
//它只顯示table1中student_no在表二當中的資訊,它可以用來替換exist語句
LeftSemiJoin ::
//等值連線操作,有些優化的內容,如果表的大小小於spark.sql.autoBroadcastJoinThreshold設定的位元組
//就自動轉換為BroadcastHashJoin,即把表快取,類似hive的map join(順序是先判斷右表再判斷右表)。
//這個引數的預設值是10000
//另外做內連線的時候還會判斷左表右表的大小,shuffle取資料大表不動,從小表拉取資料過來計算
HashJoin ::
//在記憶體裡面執行select語句進行過濾,會做快取
InMemoryScans ::
//和parquet相關的操作
ParquetOperations ::
//基本的操作
BasicOperators ::
//沒有條件的連線或者內連線做笛卡爾積
CartesianProduct ::
//把NestedLoop連線進行廣播連線
BroadcastNestedLoopJoin :: Nil
......
}
複製程式碼
這一步是把邏輯計劃轉換成物理計劃,或者說是執行計劃了,裡面有很多概念是我以前沒聽過的,網上查了一下才知道,原來資料庫的執行計劃還有那麼多的說法,這一塊需要是專門研究資料庫的人比較瞭解了。剩下的兩步就是 prepareForExecution 和 execute 操作。
prepareForExecution 操作是檢查物理計劃當中的 Distribution 是否滿足 Partitioning 的要求,如果不滿足的話,需要重新弄做分割槽,新增 shuffle 操作,這塊暫時沒咋看懂,以後還需要仔細研究。最後呼叫 SparkPlan 的 execute 方法,這裡面稍微講講這塊的樹型結構。
sql 解析出來就是一個二叉樹的結構,不管是邏輯計劃還是物理計劃,都是這種結構,所以在程式碼裡面可以看到 LogicPlan 和 SparkPlan 的具體實現類都是有繼承上面圖中的三種型別的節點的。
非 LeafNode 的 SparkPlan 的 execute 方法都會有這麼一句 child.execute(),因為它需要先執行子節點的 execute 來返回資料,執行的過程是一個先序遍歷。
最後把這個過程也用一個圖來表示吧,方便記憶。
(1) 通過一個 Parser 來把 sql 語句轉換成 Unresolved LogicPlan,目前有兩種 Parser,SqlParser 和 HiveQl。
(2) 通過 Analyzer 把 LogicPlan 當中的 Unresolved 的內容給解析成 resolved 的,這裡麵包括表名、函式、欄位、別名等。
(3) 通過 Optimizer 過濾掉一些垃圾的 sql 語句。
(4) 通過 Strategies 把邏輯計劃轉換成可以具體執行的物理計劃,具體的類有 SparkStrategies 和 HiveStrategies。
(5) 在執行前用 prepareForExecution 方法先檢查一下。
(6) 先序遍歷,呼叫執行計劃樹的 execute 方法。