關係型數據庫的工做原理(四)

查詢優化:

    現代數據庫都使用一種基於成本優化(參見第一部分)的方式進行優化查詢,這種方式的思路是給每種基本運算設定一個成本,而後採用某種運算順序總成本最小的方式進行查詢,獲得最優的結果。算法

    爲簡化理解,對數據庫的查詢重點放在查詢時間複雜度上,而不考慮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;

    該方式在數據量不大時是比較有效的,但當數據量很大時,至關於全掃描了。

    其餘獲取方式

   以oracle爲例

    鏈接

    獲取到數據後對數據進行鏈接運算,這裏介紹三種鏈接方式: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

  通常分爲兩個步驟:

  1. 讀取outer relation 每一行
  2. 檢查inner relation中的每一行是否匹配鏈接

  僞代碼:  

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放到內存,作一個改進,基本思路:

  1. 不逐行讀取兩個關係,而是分組讀取,將組信息放到內存中;
  2. 對比(內存中)的組間行,保留符合鏈接條件的行
  3. 依次加載其餘組直至對比兩關係中全部組。

     僞代碼

// 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

  

  基本思路:

  1. 獲取inner relation中的全部元素
  2. (根據inner relation中的元素)構建一個常住內存hash table
  3. 逐個獲取outer relation全部元素
  4. 計算每一個元素的哈希值(利用哈希函數計算哈希表),與inner relation中的元素逐個比較,以肯定inner relation對應哪一個bucket
  5. 肯定bucket與outer relation對應關係(buckt是否存在outer relation中元素)

  分析其時間複雜度: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更小版本:

  1. 對inner 和 outer relation都建立一個hash table
  2. 把建立的hash tables放入磁盤
  3. compare the 2 relations bucket by bucket (with one loaded in-memory and the other read row by row)

  Merge join 

  Merge join是惟一產生排序結果的鏈接查詢。

  排序

  在最開始介紹過歸併排序,能夠看到歸併排序是一個很好的算法(固然若是不考慮內存狀況下會有更好的算法,好比hash join)。但在如下條件時,通常會選擇merge join。

  1. 某個關係(表中)已經排好序
  2. 某個關係鏈接條件建有索引
  3. 鏈接條件產生的是中間結果,而該中間結果已經排序.

                             

                      Fig. 13

  

  Merge的過程和前面介紹的merge sort很類似,可是不會逐個讀取兩個關係元素,只會選擇符合鏈接條件的元素。基本思路以下:

  1. 對比兩個relations的當前元素;
  2. 若是兩個元素相等,取出該元素,對比下面的元素;
  3. 若是兩個元素不相等,將較小元素進入下一次對比。
  4. 重複以上,直到兩個relations都處理到最後一個元素。

  以上思路是在倆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

算法比較選擇:

  1. 內存的佔用:若是沒有足夠的內存,基本要告別強大的 hash join ( 至少也告別全內存 hash join)。
  2. 2個關係的數據量:好比要鏈接的兩個表,一個數據量特別巨大,一個又很小很小,這時候 nested loop join 的效果要比 hash join 好,由於 hash join 給那個數據量巨大的表建立 hash 表就很費事。 若是兩個表都有巨量的數據, nested loop join 鏈接方式的 CPU 負載會比較大;
  3. 索引的方式: 若是鏈接的兩個關係都有 B+樹索引,那確定是 merge join 效果最好;
  4. 結果是否須要排序: 若是但願此次鏈接獲得一個排序的結果(這樣就可使用 merge join 方式實現下一個鏈接),或者查詢自己(有 ORDER BY/GROUP BY/DISTINCT 運算符)要求的排序的結果;若是是這個狀況,即便當前要鏈接的 2 個關係自己並無排好序, 依然建議選擇稍微有點費事的 merge join(能夠給出排序的結果);
  5. 鏈接的 2 個關係自己已經排序: 這個狀況,必須用 merge join;
  6. 鏈接類型: 若是是等值鏈接(好比: tableA.col1 = tableB.col2)?或者是內鏈接、外鏈接、笛卡爾積、自鏈接?有些鏈接方式可能不能處理這些不一樣類型的鏈接;
  7. 數據的分佈: 若是鏈接條件的數據扭曲了(好比要鏈接表 PERSON 鏈接條件是列「姓」,可是意味的是不少人的姓是相同的),這個狀況若是使用 hash join 必定會帶來災難,對吧?由於哈希函數計算後各個 buckets 上數據的分佈確定存在巨大的問題 (有些 bucket 很小,只有一兩個元素;而有些 buckets 太大,好幾千的元素) 。

 

 下一篇將有一個簡單的例子簡要說明改過程。

相關文章
相關標籤/搜索