現代數據庫都使用一種基於成本優化(參見第一部分)的方式進行優化查詢,這種方式的思路是給每種基本運算設定一個成本,而後採用某種運算順序總成本最小的方式進行查詢,獲得最優的結果。算法
爲簡化理解,對數據庫的查詢重點放在查詢時間複雜度上,而不考慮CPU消耗,內存佔用與磁盤I/O,且相比與CPU消耗,數據庫瓶頸也更多在磁盤I/O。sql
索引數據庫
B+樹、bitmap index等都是常見的索引實現方式,不一樣的索引實現有不一樣的內存消耗、I/O以及CPU佔用。一些現代數據庫還能夠建立臨時索引。oracle
獲取(數據)方式:函數
在進行鏈接查詢操做以前,須要先獲取數據,如下是常見的獲取方式(數據獲取的關鍵在磁盤I/O,故在衡量獲取方式時,考察量也應在此)。oop
全掃描優化
全掃描(full scan or scan),即掃描整個表或者全部索引,全表掃描的磁盤I/O明顯高於全索引掃描。spa
範圍掃描code
AGE字段有索引,當sql使用謂詞where age < 20 and age >20時(between and會在上面查詢解析階段改爲<和>),便會使用範圍索引。參見第一部分知,範圍掃描的複雜度爲log(N)+M,N是索引中的數據量,M是搜索範圍內行數,可見範圍掃描比全索引掃描有更低的磁盤I/O。htm
惟一掃描
若是想只須要從索引中取一個值,可用惟一掃描(unique scan)。
根據rowid獲取
當要查詢索引行相關的列時,便會用到rowid,好比查詢age=28(age上有索引,name無索引)的人的名字:
Select name from person where age = 28;
以上的查詢會按照:查詢索引列age,過濾出age=28的全部行,而後按照查詢出來的行號查name列,即先讀索引再讀表。但下面列子就不用讀表了(name有索引):
Select location.street from person, localtion where person.name = person.name;
該方式在數據量不大時是比較有效的,但當數據量很大時,至關於全掃描了。
其餘獲取方式
獲取到數據後對數據進行鏈接運算,這裏介紹三種鏈接方式:merge join, hash join, nested loop join,以及引入inner relation和outer relation兩個概念。關係數據庫中定義了「關係」的定義,它能夠是:一個表,一個索引以及前面運算的結果。
鏈接兩個關係時,數據庫鏈接運算處理兩個關係方式可能不一樣,本文定義:
鏈接運算符左邊的關係稱爲outer relation;
鏈接運算符右邊的關係稱爲inner relation。
好比a join b,a稱爲outer relation(常看見的是外表說法),b稱爲inner relation(常看見的是內表說法)。多數狀況下 a join b 與b join a的成本是不同的。該部分假定outer relation有N個元素,inner relation有M個元素(實際狀況下,這些信息數據庫經過統計能夠知道,如上部分)。
嵌套循環鏈接(Nested loop join):
Fig. 11
通常分爲兩個步驟:
僞代碼:
nested_loop_join(array outer, array inner) for each row a in outer for each row b in inner if (match_join_condition(a,b)) write_result_in_output(a,b) end if end for end for
顯然時間複雜度爲(N*M)。從磁盤I/O考慮,算法須要從磁盤讀N+N*M行。可知,當M足夠小時,只須要讀N+M次,這樣就能夠把讀取結果放到內存中,因此通常狀況下都會將小的relation做爲inner relation。
固然這雖然改善了磁盤I/O,時間複雜度並無變化。若是進一步優化磁盤I/O,還能夠考慮將inner relation用索引來替換。
考慮儘量將inner relation放到內存,作一個改進,基本思路:
僞代碼
// improved version to reduce the disk I/O. nested_loop_join_v2(file outer, file inner) for each bunch ba in outer // ba is now in memory for each bunch bb in inner // bb is now in memory for each row a in ba for each row b in bb if (match_join_condition(a,b)) write_result_in_output(a,b) end if end for end for end for end for
該版本相比以前版本時間複雜度沒有變化,但磁盤I/O明顯變小了:number_of_bunches_for(outer)+ number_of_bunches_for(outer) * number_of_ bunches_for(inner),並且可知增長分組的大小,即每次讀取更多數據,還能繼續減少讀取次數。
哈希鏈接(hash join)
哈希鏈接更加複雜,但大多場合中比循環嵌套鏈接成本更低。
Fig. 12
基本思路:
分析其時間複雜度:inner relation分爲x個buckets,outer relation與buckets對比的次數取決於buckets中的元素個數。哈希函數對各個關係中的元素是均勻分佈的,也就是說buckets的大小是相同的。
時間複雜度:(M/X) * N + cost_to_create_hash_table(M) + cost_of_hash_function*N,當hash函數建立足夠小的buckets時,好比buckets只有一個元素,那麼時間複雜度能夠爲(M+N)。
內存佔用更小磁盤I/O更小版本:
Merge join
Merge join是惟一產生排序結果的鏈接查詢。
排序
在最開始介紹過歸併排序,能夠看到歸併排序是一個很好的算法(固然若是不考慮內存狀況下會有更好的算法,好比hash join)。但在如下條件時,通常會選擇merge join。
Fig. 13
Merge的過程和前面介紹的merge sort很類似,可是不會逐個讀取兩個關係元素,只會選擇符合鏈接條件的元素。基本思路以下:
以上思路是在倆relations已經排好序且任一關係中不存在相同元素的簡化模型下,具體的要複雜的多。
時間複雜度,若是兩個relations已經排序好,複雜度爲N+M;若是需先排序再鏈接,複雜度爲(N*log(N)+M*log(M))。
僞代碼
mergeJoin(relation a, relation b) relation output integer a_key:=0; integer b_key:=0; while (a[a_key]!=null and b[b_key]!=null) if (a[a_key] < b[b_key]) a_key++; else if (a[a_key] > b[b_key]) b_key++; else //Join predicate satisfied write_result_in_output(a[a_key],b[b_key]) //We need to be careful when we increase the pointers if (a[a_key+1] != b[b_key]) b_key++; end if if (b[b_key+1] != a[a_key]) a_key++; end if if (b[b_key+1] == a[a_key] && b[b_key] == a[a_key+1]) b_key++; a_key++; end if end if end while
算法比較選擇:
下一篇將有一個簡單的例子簡要說明改過程。