[譯]數據庫是如何工做(五)查詢管理器

這部分是數據庫的強大之處。 在這部分中,一個寫的不太好的查詢被轉換成一個快速的可執行代碼。而後執行代碼,並將結果返回給客戶端管理器。這是一個多步驟操做:html

  • 首先對查詢語句進行解析(parser),看看它是否有效的
  • 而後重寫(rewritten)去移除無用的操做並添加一些預優化
  • 而後對其進行優化(optimized),以此來提升性能,並生成一個執行和數據訪問的計劃
  • 而後計劃被編譯(compiled)
  • 最後,執行

在這部分,我不會討論最後兩點由於他們不那麼重要。 在閱讀這部分以後,若是你想了解更多我推薦你閱讀:
算法

  • 最初的基於成本(cost)優化研究論文(1979):關係數據庫管理系統訪問途徑的選擇。這篇文章只有12頁,計算機科學的平均水平就能理解了
  • 一個又好又有深度的演講,介紹 DB2 9.X 是如何優化查詢這裏
  • 很是好地介紹了PostgreSQL如何優化查詢這裏。這是一份最友好的文檔,由於它更可能是介紹「讓咱們看看 PostgreSQL 在這些狀況下的查詢計劃」 ,而不是「讓咱們看看 PostgreSQL 使用的算法」
  • SQLite講優化器的官方文檔。這是很容易閱讀的,由於 SQLite 使用的是很簡單的規則。並且,只是惟一的官方文件解釋它是怎樣工做的
  • 一個很好的演講關於 SQL Server 2005 的優化器這裏
  • 關於Oracle 12C優化的白皮書。這裏
  • 從《數據庫系統概念》一書的做者那裏,查詢優化的2個理論課程。這裏這裏 這一個很好的,重點是磁盤I/O成本,但必須有計算機科學的良好水平。
  • 另一個理論課程,這更容易讀,但只關係 join 操做符和 I/O 讀寫。

查詢解析(parser)

每條SQL語言都會被送到解析器,在那裏檢查語法是否正確。若是你的查詢語句有錯,解析器會拒絕這條語句。例如:若是你寫「SLECT ... 」 而不是 「SELECT ... 」 ,就會在立刻結束。sql

解析不只僅是檢查單詞拼寫,還會更深刻。它還檢查關鍵字是否按正確的順序使用。例如,WHERE 在 SELECT 以前也會拒絕。數據庫

而後,對查詢中的表和字段進行分析。解析器會使用數據庫的元數據(metadata)來檢查:編程

  • 表是否存在
  • 表中字段是否存在
  • 那些操做是否能夠對那些類型字段使用(好比:你不能夠用整數與字符串進行比較,就不能對整數使用substring()函數)

接着,它會檢查你是否用讀表(或寫表)的受權。一樣的,這些表的訪問權限由你的DBA設置的。 在解析的時候,SQL查詢語句會被轉換成內部的表示(一般是樹)數組

若是一切正常,就會把內部表示發送給查詢重寫器緩存

查詢重寫器

在這步,我有一個查詢語句的內部表示。重寫的目標是架構

  • 對查詢預優化
  • 避免沒必要要的操做
  • 幫助優化器尋找最佳的解決方案

這重寫器執行 這重寫器會對查詢(query,這裏應該是說內部表示)執行一系列已知的規則。若是這查詢匹配到規則,就會用這個規則重寫。一下是一些(可選)的非詳盡的規則:oracle

  • 視圖合併: 若是你在查詢中用到視圖,這視圖會轉成視圖的SQL代碼
  • 子查詢的展開:子查詢是很是難優化的,因此重寫器會嘗試用修改查詢的方式來刪除子查詢

舉個例子app

SELECT PERSON.*
    FROM PERSON
    WHERE PERSON.person_key IN
    (SELECT MAILS.person_key
    FROM MAILS
    WHERE MAILS.mail LIKE 'christophe%');

或被替換成

SELECT PERSON.*
    FROM PERSON
    WHERE PERSON.person_key IN
    (SELECT MAILS.person_key
    FROM MAILS
    WHERE MAILS.mail LIKE 'christophe%');
  • 去除沒必要要的操做:舉個例子,若是你用了 DISTINCT 而你又有一個 UNIQUE 的惟一性約束來阻止數據的不惟一,那就就會刪除 DISTINCT 的關鍵字
  • 消除冗餘關聯:若是由於有一個 JOIN 被視圖隱藏,而存在兩個JOIN條件或者經過關聯的傳遞性就能夠獲得等價,有鏈接是沒用的,就會將其刪除。
  • 常量的計算: 你若是你寫了一些東西是須要運算的,那麼他會在重寫階段就算出來的。好比, WHERE AGE > 10 + 2會被轉換成WHERE AGE > 12 ,仍是 TODATE("some date") 會被轉成日期時間(datetime)的格式
  • (高級) 分區裁決:若是使用分區表,重寫器能夠找到要使用的分區。
  • (高級)物化視圖重寫:若是你有個物化視圖在查詢中匹配到謂詞子集,則重寫器會檢查視圖是不是最新的,並修改查詢以使用物化視圖而不是原始表。
  • (高級) 多維分析轉化:分析/窗口函數,星形鏈接,彙總...也被轉換(但我不肯定它是由重寫器仍是優化器完成的,由於兩個進程都很是接近,它必須依賴於數據庫)

而後會將這個重寫的查詢發送到查詢優化器,開始有趣起來了!

統計

在咱們看到數據庫是若是優化查詢以前,咱們須要討論一下統計由於沒有了他,數據庫是一個愚蠢的 。若是你沒有告訴數據庫要分析它本身的數據,它是不用去作的,並且它會作出很是糟糕的假設 可是什麼樣的信息是數據庫須要的呢? 我不得不(簡要)地談論數據庫和操做系統是如何存儲數據的。他們都使用一個最小的單元叫頁(page) 或者塊(block)(默認是4或者8KB。這意味着若是你須要 1KB不管如何起碼都會用掉你一頁了。若是你一頁是 8KB,就會浪費掉 7KB 回到統計!當你要求數據庫要收集統計信息時,他會計算這些值:

  • 表有多少行/頁
  • 表中的每一列(column)
    • 全部數據的惟一值(distinct data value)
    • 數據值的長度(最大,最小,平均)
    • 數據值的範圍(最大,最小,平均)
  • 有關表索引的信息

這些統計會幫助優化器預估查詢的磁盤 I/O、CPU 和內存使用狀況 每列的統計信息都很重要。舉個栗子,若是有一個 PERSON 的表格就要關聯(JOIN)兩個列: LAST_NAME , FIRST_NAME 。經過統計,數據庫知道只有 FIRST_NAME 只有 1,000 不一樣值,而 LAST_NAME 有 1,000,000 個不一樣值(每一個人都有FIRST_NAME和LAST_NAME,量是同樣)。因此數據庫會按 LAST_NAME,FIRST_NAME 的順序連到數據上,而不是按 FIRST_NAME,LAST_NAME 的順序。這能減小不少對比由於 LAST_NAME 大多不相同,由於不少是時候只要對比前面 2(或者3) 個 LAST_NAME 的字符就夠了。 但這些是基礎統計。你能夠要求數據庫計算很高級的數據叫柱狀圖(histograms)。柱狀圖能夠統計列中的(column)值的分佈的信息。例如:

  • 最頻繁使用的值
  • 位數
  • ...

這額外的統計會幫助數據庫訊號一個很好的查詢計劃。特別是對於等式謂詞(如:WHERE AGE = 18)或者是範圍謂詞(如:where AGE > 10 AND AGE < 40)由於這數據庫能更好地知道這些謂詞涉及的行數 這些統計會存儲在數據庫的元數據(metadata)中。例如你能夠看到表(非分區表)的這些統計:

  • Oracle 中的 USER/ALL/DBA_TABLES 和 USER/ALL/DBA_TAB_COLUMNS
  • 在 DB2 中的 SYSCAT.TABLES 和 SYSCAT.COLUMNS

這些統計必須是最新的。沒有什麼比數據庫認爲表只有 500 行實際上有 1,000,000行更糟糕了。統計的惟一缺點是須要時間去計算。 這就是大部分數據庫默認不是自動計算的緣由。數百萬的數據讓計算變得很困難。在這種狀況下,你能夠選擇只計算基本統計信息或者按事例數據那樣統計信息

舉個栗子,當我處理每一個表都有百萬行的數據的項目時,我選擇只計算10%的統計信息,這讓我耗費巨大的時間。事實證實是個糟糕的決定。由於有時候在ORACLE 10G 給特定的表的特定的列選擇統計10%和統計全部的100%是很是不一樣的(對於一百萬個行之內的表不太可能發生)。這個錯誤的統計致使有時一個查詢要用8個小時而不是30秒,並且尋找根源也是個噩夢。這個例子讓咱們看到統計是多麼的重要。

注意:固然,每一個數據庫都有有不少高級的統計特性。若是你想知道更多,要看下數據可的文檔。正如我所言,我會嘗試明白如何使用統計而我發現一個最官方的文檔是 PostgreSQL。

查詢優化器

全部現代數據庫都用基於成本的優化(CBO)去優化查詢。這思想是每一個操做都放一個成本值,經過用最少成本的操做獲取結果的方式,來尋找下降查詢成本最好的方式。

爲了明白一個基於成本的優化器是如何工做的,用一個去例子「感覺」下這個任務背後的複雜度,我想應該是不錯的。在這部分,我將爲你介紹鏈接2個表的3種普通方法,而且咱們將能很看到,一個簡單的鏈接查詢優化也是個噩夢。在此以後,咱們將看到真正的優化器上是如何工做的。

關於這些關聯,我將會把重點放在他們的時間複雜度,但數據庫的優化器會計算它們的CPU成本、磁盤 I/O 和內存需求。時間複雜度和CPU的成本是很是接近的(對於像我同樣懶的人來說)。對於 CPU 的成本,我應該計算每一個操做箱加法,「if語句」,一個乘法,迭代。。。 更多:

  • 每一個高級代碼操做有特定數量的低級CPU操做符
  • 每種CPU的操做符的成本是不一樣的(在 CPU週期),不管你使用 Inter Core I7,Inter 奔騰4,仍是 AMD 的 opton , 換句話說,(CPU操做符的成本)取決於CPU的架構

使用時間複雜度更容易(至少對於我來說),而使用它咱們仍然能獲得 CBO 的概念。我有時會講磁盤 I/O,由於這也是個重要的概念。值得注意的是,大多數的瓶頸是磁盤I/O而不是CPU的使用率

索引

當看到 B+ 樹的時候,咱們會談及 B+ 樹。要記得,索引都是已排序的

僅供參考,還有不少其餘類型的索引,好比位圖索引(bitmap indexs)。與B +樹索引相比,它們在CPU,磁盤I / O和內存方面的成本不一樣。

此外,不少現代的數據庫中 ,若是動態建立臨時索引能改善執行計劃的成本,就會對當前的查詢使用。

訪問方式

在使用你的關聯操做符(JOIN)以前,你首先要獲取你的數據。下面是如何獲取數據的方式

注意:由於訪問途徑的實際的問題是磁盤 I/O,因此我不會討論太多時間複雜度

全局掃描

若是你讀過執行計劃,你會確定曾經看過這個單詞全局掃描full scan 或只是掃描)。全局掃描是數據庫讀表或者完整的索引的簡單方式。對磁盤I/O來說,一個表的全局掃描的成本明顯是比一個索引掃描貴得多。

範圍掃描

有不少其餘類型的掃描,如索引範圍掃描。例如:當你使用謂詞如「WHERE AGE > 20 AND AGE < 40」 固然若是你須要有一條 AGE 字段的索引你才能使用。 咱們已經在第一部分中看到,範圍查詢的時候複雜度大概是 log(N)+M, 其中 N 是索引中數據的數量,而 M 大概是這個範圍內的行數。感謝統計信息,M 和 N 都是已知的(注意:對於謂詞 AGE > 20 AND AGE < 40 來說,M是可選擇性的)。此外,對於範圍掃描來說,你無需讀全局索引 ,它在磁盤I/O上比全局掃描的成本更低

惟一掃描

若是你只須要索引中的一個值,則可使用惟一掃描。

經過行ID訪問

大多數狀況下,若是數據庫使用索引,則必須查找與索引關聯的行。爲此,它將使用行ID訪問。 例如,若是你作像這樣的事

SELECT LASTNAME, FIRSTNAME from PERSON WHERE AGE = 28

若是你 PERSON 表中有字段 AGE 的索引,優化器將使用索引來查找年齡是 28 的全部人,並詢問表中的關聯行,由於索引只有關於年齡的信息,而你想知道姓氏和名字。 但若是你如今作事是

SELECT TYPE_PERSON.CATEGORY from PERSON ,TYPE_PERSON
    WHERE PERSON.AGE = TYPE_PERSON.AGE

PERSON上的索引將用於與TYPE_PERSON關聯,但因爲你沒有詢問表 PERSON 的信息,所以不會經過行ID訪問表PERSON。 雖然它在少許訪問的時候很是好用,但此操做的真正問題是磁盤I/O。若是你按行ID進行過多訪問,則數據庫可能會選擇完整掃描。

其餘訪問方式

我沒有介紹全部訪問路徑。若是您想了解更多信息,能夠閱讀 Oracle文檔。其餘數據庫的 名稱可能不一樣,但背後的概念是相同的。

JOIN 操做符

因此,咱們知道如何獲取咱們的數據,讓咱們來 JOIN 下他們吧! 我將介紹3個常見的關聯運算符:合併關聯(Merge Join)哈希關聯(Hash Join)嵌套循環關聯(Nested Loop Join) 但在此以前,我須要引入新的詞彙:內部聯繫(inner relation)外部聯繫(outer relation)。聯繫能夠是:

  • 一個表
  • 一個索引
  • 來自上次操做的中間結果(如:前一個關聯操做的結果)

當你正在關聯兩個聯繫時,關聯算法會有兩種不一樣方式管理這兩種聯繫。在本文的剩餘部分,我會假設:

  • 外部聯繫是左側的數據集
  • 內在聯繫是右側的數據集

舉個栗子,A JOIN B 就是 A 和 B 之間的關聯 ,而 A 就是外部聯繫,B就是內部聯繫。 大多數時候,A JOIN B 的成本和 B JOIN A 的成本是不同的 在這部分,我也會假設外部聯繫有 N 個元素,內部關係有 M 個元素。請記得,真正的優化器會經過信息統計知道 N 和 M 的值。 注意:N 和 M 的關係是基數(cardinalities)

嵌套循環關聯

嵌套循環關聯是最簡單的

這是是它的思想:

  • 首先遍歷外部聯繫的每一行
  • 而後查看在內部關係中的全部看,看一下是否存在能匹配的行

下面是僞代碼:

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

因爲它是雙重迭代,時間複雜度爲O(N * M)

就磁盤 I/O 而言, 外部關係中(N行)要找每行的(對應關係),都須要內部關係中循環 M 遍。因此這算法須要從磁盤中讀取 N + N*M 次。可是,若是內部關係是足夠小的,你能夠把內部關係放到內存中,那麼你只須要讀取磁盤 N+M 次了(M的數據寫到內存)。這種修改,內部聯繫必定是最小的,由於這才能更好放進內存

對於時間複雜度而言,沒有區別,但對磁盤I/O來說,上面那種方式更好。兩條數據創建聯繫都只需讀一次磁盤。

固然,內部關聯能夠被索引代替,這會對磁盤 I/O 更好

因爲這個算法是很簡單的, 若是內部關聯太大以至於不易放進內存,這是另外一個對磁盤更友好的版本。構思以下:

  • 不要逐行讀取兩個聯繫
  • 一塊一塊地讀數據,並保持有兩塊數據(來自每一個聯繫)在內存中
  • 比較兩塊數據,保留匹配到的數據,
  • 而後再從磁盤中建立新的塊,並對比
  • 直到沒有塊能夠加載

下載是可能的算法

//改進版本以減小磁盤I / O.
nested_loop_join_v2(file outer, file inner)
    for each bunch ba in outer
    // ba 如今在內存中了
        for each bunch bb in inner 
        // bb 如今在內存中了
           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

使用此版本時,時間複雜度保持不變,但磁盤訪問次數減小:

  •  先前版本,算法須要N + N * M個訪問(每一個訪問得到一行)。
  • 使用此新版本,磁盤訪問次數變爲 外部塊的數量 + 外部塊的數量 * 內部塊的數量
  • 若是增長每一個塊的大小,則減小磁盤訪問次數。

注意:每一個磁盤訪問都會採集比之前算法更多的數據,但這並不重要,由於它們是順序訪問(機械磁盤的真正問題是獲取第一個數據的時間)。

哈希關聯

哈希關聯的思想是:

1) 從內部關聯中獲取全部元素
2) 建立內存哈希表
3) 逐一獲取外部關係中的全部元素
4) 計算哈希表的每一個元素的哈希碼(經過哈希表的哈希函數),用於尋找對應的內部聯繫桶(bucket)
5) 查看桶中的元素是否和外部表的元素匹配 對於時間複雜度來說,我須要 假設 一些東西去簡化問題:

  • 內部聯繫被分紅 X 個桶
  • 哈希函數幾乎均勻地分佈哈希值,換句話來說,每一個桶的大小是相同的
  • 外部聯繫一個元素和桶中全部元素的匹配的時間複雜度是桶中元素數量(M/X)

全部總共時間複雜度:(M/X) * N + 建立哈希表的成本(M) + 哈希函數的成本 * N 。 若是哈希函數建立了哈希桶的大小足夠小(size小,桶數更多,X越大),那麼複雜度就是 O(M+N)? 這是哈希鏈接的另外一個版本,更節省內存但對磁盤 I/O不友好,此次:
1) 內部和外部聯繫都計算哈希表
2) 而後將他們放進磁盤
3) 而後逐個桶比較二者的關係(一個用加載到內存,另外一個逐行讀文件) [原文的意思是外部聯繫的全部元素哈希值存在一個文件中,逐行讀取。經過哈希值能夠知道是幾號桶,就把桶加載到內存進行對比]

合併關聯

合併鏈接是惟一一種關聯能夠生成排序結果

注意:在這個簡化的合併關聯中,不區份內部或外部表;二者都扮演了同樣的角色。但實際的實現起來是有不一樣的,例如,在處理重複項時。

排序

咱們已經講過了合併排序,在這種狀況下合併排序是個好的算法(但若是內存足夠,就不是最好的) 就是數據集是已排序的,舉個栗子:

  • 若是表內部原本就有序的,好比關聯條件中的索引組織表(oracle的,表中的數據按主鍵存儲和排序)
  • 若是關聯條件的聯繫用的是索引
  • 若是關聯的數據是在查詢過程當中已排序的中間結果

合併關聯

這部分很是相似於咱們看到的合併排序的合併操做。
但這一次,咱們只選擇兩個關係中相等的元素,而不是從兩個關係中挑選每一個元素。

這是它的構思:
1) 對比兩個關係中當前項(初始的時候兩個當前項都是第一個)
2) 若是他們是相等的,就把兩個元素放到結果,再比較兩個關係的下一個元素
3) 若是不相等,就去對比值最小的那個關係的下個元素(由於下個元素可能能匹配)
4) 重複 1,2,3 直到有個關係到達最後一個元素

這是有效的,由於兩個關係都是已排序的,因此你不須要在這些關係中返回。

這算法一個簡化版,由於它沒有處理數組中有多個相同值的狀況(換句話說,多重匹配)。針對這種狀況,真實的版本過重複了。這就是我選擇簡化版的緣由。

若是兩個關係是已排序的,那麼事件負責會是 O(N+M)

若是兩個關係都須要排序,那麼會加上排序的成本,時間複雜度會是 O(N*Log(N) + M*Log(M))

對計算機科學的Geeks 來說,這裏有一個可能的算法來處理多個匹配(注意:我不是100%確定個人算法):

mergeJoin(relation a, relation b)
    relation output
    integer a_key:=0;
    integer b_key:=0;

    while (a[a_key]!=null or 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
        //i.e. a[a_key] == b[b_key]

            //計算與a相關的重複數量
            integer nb_dup_in_a = 1:
            while (a[a_key]==a[a_key+nb_dup_in_a])
                nb_dup_in_a++;

            //計算與b相關的重複數量
            integer dup_in_b = 1:
            while (b[b_key]==b[b_key+nb_dup_in_b])
                nb_dup_in_b++;

           //在輸出中寫下重複項
           for (int i = 0 ; i< nb_dup_in_a ; i++)
               for (int j = 0 ; i< nb_dup_in_b ; i++)
                   write_result_in_output(a[a_key+i],b[b_key+j])

          a_key=a_key + nb_dup_in_a-1;
          b_key=b_key + nb_dup_in_b-1;

        end if
    end while

哪個是最好的?

若是存在最佳類型的關聯,就不會有那麼多種類型。這個問題很是困難,由於有不少因素髮揮做用:

1) 有大量的空閒內存:沒有足夠的內存,你能夠和強大的哈希關聯說再見了(至少和全在內存中進行散列鏈接的方式說再見)
2) 2個數據集的大小:舉個栗子,若是你有一個很是大的表要關聯個小表,嵌套循環關聯會比哈希關聯快,那是由於哈希關聯的建立成本較高。若是你有兩個很大的表,那麼嵌套查詢就比較耗CPU了
3) 索引的存在:若是是兩個B+樹的索引,最機智的選擇固然是合併關聯了
4) 若是結果須要排序:即便你正在處理的數據集是沒排序的,你可能會想使用成本昂貴的合併關聯(用來排序),由於合併關聯後結果是有序的,你也能夠把它和其餘的合併關聯連起來用(或者由於使用 ORDER BY/GROUP BY/DISTINCT 等操做符隱式或顯式地要求一個排序結果)
5) 若是關係已經排序:在這種狀況下,合併鏈接是最佳候選
6) 關聯的類型: 它是等值鏈接(即:tableA.col1 = tableB.col2)?它是內關聯外關聯笛卡爾積仍是自關聯?某些關聯在某些狀況下沒法工做。
7) 數據的分佈: 若是數據在關聯條件下是有偏向的(好比根據姓氏來關聯人,可是不少人同姓),使用哈希關聯將是一場災難,由於哈希函數將建立分佈不均勻的桶。
8) 若是但願鏈接由 多個線程/進程 執行 有關更多信息,您能夠閱讀DB2ORACLESQL SERVER文檔。

簡單的例子

咱們剛剛看到了3種類型的關聯操做。 如今,咱們須要關聯5個表來表示一我的的信息。一我的要可能有:

  • 多個電話
  • 多個電子郵箱
  • 多個地址(多是個土豪)
  • 多個銀行帳號

換個話說,咱們須要用下面的查詢快速獲得答案

SELECT * from PERSON, MOBILES, MAILS,ADRESSES, BANK_ACCOUNTS
    WHERE
        PERSON.PERSON_ID = MOBILES.PERSON_ID
        AND PERSON.PERSON_ID = MAILS.PERSON_ID
        AND PERSON.PERSON_ID = ADRESSES.PERSON_ID
        AND PERSON.PERSON_ID = BANK_ACCOUNTS.PERSON_ID

做爲查詢優化器,我必須找處處理數據的最佳方法。可是有兩個問題:

每次關聯應該使用什麼樣類型?

我有3個可能的關聯(哈希關聯,合併關聯,嵌套關聯),有可能使用 0,1 或者 2個索引(更不用說有不一樣類型的索引)

我應該選擇什麼順序來計算關聯?

例如,下圖顯示了4個表上3個關聯的可能不一樣狀況

因此這是我以爲的可能性:
1) 我用暴力遍歷的方式 使用數據庫的統計信息,我計算每一個方案的成本並獲得最好的方案,但這也太多中方案了吧。對於給定的關聯順序,每一個關聯有3個可能性: 哈希關聯、合併關聯、嵌套關聯。因此會有 3^4 中可能性。肯定關聯的順序是個二叉樹排列問題,有會有 (2*4)!/(4+1)! 種可能的順序。而本例這這個至關簡單的問題,我最後會獲得 3^4*(2*4)!/(4+1)! 種可能。 拋開專業術語,那至關於 27,216 種可能性。若是給合併聯接加上使用 0,1 或 2 個 B+樹索引,可能性就變成了 210,000種。我忘了提到這個查詢很是簡單嗎?
2) 我哭了,並退出這個任務 這很誘人,但你也不獲得你想要結果,畢竟我須要錢來支付帳單。
3) 我只嘗試幾種計劃並採起成本最低的計劃。 因爲我不是超人,我沒法計算每一個計劃的成本。 相反,我能夠任意選擇全部可能計劃的子集,計算其成本併爲你提供該子集的最佳計劃。
4)我使用智能規則來減小可能的計劃數量 下面有兩種的規則: 1. 我可使用「邏輯」規則來消除無用的可能性,但它們不會過濾不少方案。好比:內部關係要用循環嵌套關聯必定要是最好的數據集 2. 我能夠接受不尋找最好的方案並用更積極的規則減小大量的可能性。好比:如何關係不多,使用循環嵌套關聯並永遠不使用合併關聯或者哈希關聯 在這個簡單的例子中,我最終獲得不少的可能性。但  現實中的查詢還會有其餘關係運算符,像 OUTER JOIN, CROSS JOIN, GROUP BY, ORDER BY, PROJECTION, UNION, INTERSECT, DISTINCT … 這意味着更多的可能性。 那麼,數據庫是如何作到的呢?

動態規劃、貪心算法和啓發式算法

關係數據庫嘗試過我剛纔說過的多種方法。 大多數狀況下,優化器找不到最佳解決方案,而是「好」解決方案 對於小型查詢,能夠採用暴力遍歷的方法。可是有一種方法能夠避免沒必要要的計算,所以即便是中等查詢也可使用暴力方法。這叫爲動態規劃編程。

動態規劃

它們用了相同的(A JOIN B)子樹。因此,咱們能夠只計算這棵樹一次,保存這樹的城下,當看到這棵樹的時候再次使用,而不是每次看到這棵樹都從新計算一次。更正規地說,咱們面對的是重複計算的問題。爲了不額外計算結果的部分,咱們使用了記憶術。

使用了這個技術,再也不是 (2*N)!/(N+1)! 的時間複雜度了,咱們只有 3^N 。在咱們以前的例子中有4個鏈接,這意味着 336個關聯順序會降到 81 個。若是你有一個大的查詢有 8 個關聯(也不是很大),這意味着會從 57,657,600 降到 6561

對於計算機科學的 GEEKS。這裏有個算法,是在我曾經介紹給你正式課程找到的。我不會去解釋這個算法,因此只有你已經明白動態規劃編程或者你算法很好(你已經被警告過了)時纔去讀它:

procedure findbestplan(S)
if (bestplan[S].cost infinite)
   return bestplan[S]
// else bestplan[S] has not been computed earlier, compute it now
if (S contains only 1 relation)
         set bestplan[S].plan and bestplan[S].cost based on the best way
         of accessing S  /* Using selections on S and indices on S */
     else for each non-empty subset S1 of S such that S1 != S
   P1= findbestplan(S1)
   P2= findbestplan(S - S1)
   A = best algorithm for joining results of P1 and P2
   cost = P1.cost + P2.cost + cost of A
   if cost < bestplan[S].cost
       bestplan[S].cost = cost
      bestplan[S].plan = 「execute P1.plan; execute P2.plan;
                 join results of P1 and P2 using A」
return bestplan[S]

對於大型查詢你仍然能夠用動態規劃處理,可是還得要用額外的規則(或啓發式算法)來消除可能性

  • 若是咱們只分析特定類型的方案。(例如:左深樹 left-deep-tree),咱們會獲得 n*2^n 而不是 3^n

  • 如何咱們在添加邏輯規則時避免一些模式(如:一個表有給定謂詞的索引,就不是嘗試用合併關聯表而要只關聯索引) 這會減小不少可能性,對最佳方案也不會形成很大的傷害。
  • 若是給流程添加規制(像是全部其餘關係操做前執行關聯操做) 這會減小不少可能性。

貪婪算法

但對一個大型的查詢或者要快速獲得答案(但查詢速度不太快),就會有使用另外一種類型的算法,叫貪婪算法。

想法是經過一個規則(或者啓發)以增量的方式構建查詢計劃。使用這個規則。貪婪算法能每次每步地找到問題的最好解決方案。算法從一個關聯開始查詢計劃,而後每一步,算法會使用經過的規制把新的關聯添加到新的查詢計劃

咱們舉個簡單的例子吧。假設咱們有個 5張表(A,B,C,D和E) 和 4次關聯。爲了簡化問題,咱們用可能會用到嵌套關聯。如今使用規則是 「用最小成本關聯」

  • 咱們首先在5張表中任意選擇一個(選A吧)
  • 咱們計算每種與A關聯的成本(A多是內部聯繫或外部聯繫)
  • 咱們發現 A 和 B 關聯的成本最低
  • 而後對比每種與 A JOIN B的結果 的成本(A JOIN B多是內部聯繫或者外部聯繫)
  • 咱們發現(A JOIN B)再關聯C有最低的成本
  • 再每種與 (A JOIN B) JOIN C 的成本
  • 。。。
  • 最後,咱們會獲得一個查詢計劃 (((A JOIN B) JOIN C) JOIN D) JOIN E)

咱們是從A開始的,咱們用一樣的算法應用到 B 上 ,而後 C ,而後 D,而後 E。咱們能夠保持這個計算是最少成本的。

順便說一下,這個算法的名字叫最近鄰居法

我不會詳細介紹,但上面的問題(可能性不少的問題) 經過良好的建模和在 複雜度是 N*log(N) 的排序就能很容易地解決。 而這個算法的成本在 O(N*log(N))。而徹底動態規劃的版本要用 O(3^N) 。若是是有20個鏈接的大查詢,則意味着26 VS 3,486,784,401,這是一個巨大的差別!

這算法的問題是,咱們假設了:找到2個表的最佳關聯,保存這個關聯,下個新關聯加進來,也會是最佳的成本,但:

  • 即便在 A, B, C 之間,A JOIN B 可得最低成本
  • (A JOIN C) JOIN B 也許比 (A JOIN B) JOIN C 更好。

爲了改善這狀況,您能夠基於不一樣的規則運行多個貪婪算法並保持最佳計劃。、

其餘算法

[若是你已經厭倦了算法,請跳到下一部分,這裏說的對於文章的其他部分並不重要]

尋找最好的可能性計劃對於計算機科學的研究者來說是一個活躍的話題。他們常常爲特定的問題/模式嘗試尋找更好的的解決方案。例如:

  • 若是查詢是星型關聯(這是某種屢次關聯查詢),某些數據庫使用一種特定的算法。
  • 若是查詢是並行查詢,則某些數據庫將使用特定算法

還研究了其餘算法來替換大型查詢的動態規劃。貪婪算法屬於稱爲啓發式算法的你們族。貪心算法遵循規則(或啓發式),保留在上一步找到的解決方案並將它追加到當前步驟的解決方案中。一些算法遵循規則並逐步應用(apply),蛋不用老是保留上一步的最佳解決方案。他們統稱啓發式算法。

好比,遺傳算法遵循規則,但最後一步的最佳解決方案一般不會保留:

  • 解決方案表示可能的完整查詢計劃
  • 每一步保留了P個方案(即計劃),而不是一個
  • 0) P個計劃隨機建立
  • 1) 成本最低的計劃纔會保留
  • 2) 這些最佳計劃合起來產生P個新計劃
  • 3) 一些新的計劃被隨機改寫
  • 4) 1,2,3步重複 T 次
  • 5) 而後在最後一次循環,從P個計劃裏獲得最佳計劃。

循環次數越多,計劃就越好。 這是魔術?

不,這是天然法則:適者生存!

實現了遺傳算法,但我並無知道它會不會默認使用這種算法的。

數據庫中還使用了其它啓發式算法,像『模擬退火算法(Simulated Annealing)』、『交互式改良算法(Iterative Improvement)』、『雙階段優化算法(Two-Phase Optimization)』…..不過,我不知道這些算法如今是否在企業級數據庫應用了,仍是隻是用於研究型數據庫。

若是想了解更多,你能夠讀這篇文章,它還介紹更多可能用到的算法《數據庫查詢優化中關聯排序問題的算法綜述》

現實中的優化器

[ 能夠到下一部分,這裏不過重要 ]

但,全部的嘰裏呱啦都是很是理論化的。由於我是開發者而不是研究員,我喜歡具體的例子。

咱們來看看SQLite 優化器是如何工做的。這是一個很輕型的數據庫,因此他使用優化器基於貪心算法+額外規則來限制可能性數量

  • SQLite 在有交叉關聯(CROSS JOIN 如: SELECT * FROM A,B)操做符的時候從不會給表從新排序
  • 關聯都用嵌套鏈接實現
  • 外聯(outer JOIN)始終按照順序評估
  • ...
  • 3.8.0版本以前,SQLite 使用「最近鄰居」算法來搜索最佳查詢計劃

等一下,咱們已經看過這個算法了。多麼地巧合 ,從3.8.0版本(發佈於2015年)開始,SQLite使用『N最近鄰居』貪婪算法來搜尋最佳查詢計劃

接下來,讓我看看其餘的優化器是如何工做的。IBM 的 DB2 看起來和其餘企業級別的數據庫同樣,我會關注 DB2 是由於我切換到大數據以前,最後使用的數據庫。

若是咱們看它的官方文檔,咱們瞭解到 DB2 的優化器容許你使用7中不一樣級別的優化:

  • 對關聯使用貪婪算法
    • 0 最小優化,使用索引掃描和嵌套循環關聯並避免一些查詢的重寫
    • 1 低級優化
    • 2 全局優化
  • 對關聯使用動態規劃
    • 3 適度優化並粗略的近似估計
    • 5 全局優化並使用啓發式的全部技術
    • 7 全局優化和5類似,但不用啓發式
    • 9 最大程度的優化,不節省計算力/成本,考慮關聯的全部順序的可能性,包括笛卡爾乘積

咱們可能看到 DB2 使用貪心算法和動態規劃 。固然,因爲查詢優化是數據庫的主要功能,它們不會分享他們用的啓發算法。

僅供參考, 默認的級別是 5 。默認的優化器使用下面的特性:

  • 使用全部可用的統計信息 ,包括頻率的值和分數位的統計
  • 使用全部查詢的重寫規則 (包括 物化查詢表路由materialized query table routing),除了再很是罕見的狀況要用計算密集的規則外。
  • 使用 動態規劃枚舉關聯 ,用於
    • 有限使用組合的內在聯繫
    • 涉及查找表的星型模式,有限使用笛卡爾乘積
  • 考慮各類的訪問方式,含列表預取(list prefetch,注:咱們將會看這是什麼),index ANDing(注:一種對索引的特殊操做),和物化查詢表路由。

默認狀況下, DB2 對關聯順序使用受限制啓發的動態規劃算法

查詢計劃緩存

因爲建立查詢計劃會花不少時間,因此絕大部分數據庫會存儲 *查詢計劃的緩存* 避免沒有必要的重複計算。這是個大話題啊,由於數據須要知道什麼時間要更新過期的計劃。解決這問題的想法是設置閾值,當統計表的改動超過某個閾值,就會從將這個表從緩存中清除

查詢執行器

在這階段,咱們有優化器的執行計劃了。這計劃會被編譯成一個可執行代碼。而後,若是有足夠的資源(內存,CPU)就會經過查詢執行器執行。在這計劃中的操做(JOIN,SORT BY...)可能會串行或者並行執行;這取決於執行器。爲了獲取和寫入數據,查詢執行器會和數據管理器互相配合,這就是這篇文章下一部分要講的

相關文章
相關標籤/搜索