查詢優化器是關係數據庫系統的核心模塊,是數據庫內核開發的重點和難點,也是衡量整個數據庫系統成熟度的「試金石」。算法
查詢優化理論誕生距今已有四十來年,學術界和工業界其實已經造成了一套比較完善的查詢優化框架(System-R 的 Bottom-up 優化框架和 Volcano/Cascade 的 Top-down 優化框架),但圍繞查詢優化的核心難題始終沒變——如何利用有限的系統資源儘量爲查詢選擇一個「好」的執行計劃。數據庫
近年來,新的存儲結構(如 LSM 存儲結構)的出現和分佈式數據庫的流行進一步加大了查詢優化的複雜性,本文章結合 OceanBase 數據庫過去近十年時間的實踐經驗,與你們一塊兒探討查詢優化在實際應用場景中的挑戰和解決方案。緩存
SQL 是一種結構化查詢語言,它只告訴數據庫」想要什麼」,可是它不會告訴數據庫」如何獲取」這個結果,這個"如何獲取"的過程是由數據庫的「大腦」查詢優化器來決定的。在數據庫系統中,一個查詢一般會有不少種獲取結果的方法,每一種獲取的方法被稱爲一個"執行計劃"。給定一個 SQL,查詢優化器首先會枚舉出等價的執行計劃。架構
其次,查詢優化器會根據統計信息和代價模型爲每一個執行計劃計算一個「代價」,這裏的代價一般是指執行計劃的執行時間或者執行計劃在執行時對系統資源(CPU + IO + NETWORK)的佔用量。最後,查詢優化器會在衆多等價計劃中選擇一個"代價最小"的執行計劃。下圖展現了查詢優化器的基本組件和執行流程。併發
查詢優化自從誕生以來一直是數據庫的難點,它面臨的挑戰主要體如今如下三個方面:框架
挑戰一:精準的統計信息和代價模型運維
統計信息和代價模型是查詢優化器基礎模塊,它主要負責給執行計劃計算代價。精準的統計信息和代價模型一直是數據庫系統想要解決的難題,主要緣由以下:分佈式
一、統計信息:在數據庫系統中,統計信息蒐集主要存在兩個問題。首先,統計信息是經過採樣蒐集,因此必然存在採樣偏差。其次,統計信息蒐集是有必定滯後性的,也就是說在優化一個 SQL 查詢的時候,它使用的統計信息是系統前一個時刻的統計信息。高併發
二、選擇率計算和中間結果估計:選擇率計算一直以來都是數據庫系統的難點,學術界和工業界一直在研究能使選擇率計算變得更加準確的方法,好比動態採樣,多列直方圖等計劃,可是始終沒有解決這個難題,好比鏈接謂詞選擇率的計算目前就沒有很好的解決方法。性能
三、代價模型:目前主流的數據庫系統基本都是使用靜態的代價模型,好比靜態的 buffer 命中率,靜態的 IO RT,可是這些值都是隨着系統的負載變化而變化的。若是想要一個很是精準的代價模型,就必需要使用動態的代價模型。
挑戰二:海量的計劃空間
複雜查詢的計劃空間是很是大的,在不少場景下,優化器甚至沒辦法枚舉出全部等價的執行計劃。下圖展現了星型查詢等價邏輯計劃個數(不包含笛卡爾乘積的邏輯計劃),而優化器真正的計劃空間還得正交上算子物理實現,基於代價的改寫和分佈式計劃優化。在如此海量的計劃空間中,如何高效的枚舉執行計劃一直是查詢優化器的難點。
挑戰三:高效的計劃管理機制
計劃管理機制分紅計劃緩存機制和計劃演進機制。
一、計劃緩存機制:計劃緩存根據是否參數化,優化一次/老是優化以及是否緩存能夠劃分紅以下圖所示的三種計劃緩存方法。每一個計劃緩存方法都有各自的優缺點,不一樣的業務需求會選擇不一樣的計劃緩存方法。在螞蟻/阿里不少高併發,低時延的業務場景下,就會選擇參數化+優化一次+緩存的策略,那麼就須要解決不一樣參數對應不一樣計劃的問題(parametric query optimization),後面咱們會詳細討論。
二、計劃演進機制:計劃演進是指對新生成計劃進行驗證,保證新計劃不會形成性能回退。在數據庫系統中, 新計劃由於一些緣由(好比統計信息刷新,schema版本升級)無時無刻都在才生,而優化器由於各類不精確的統計信息和代價模型始終是沒辦法百分百的保證新生成的計劃永遠都是最優的,因此就須要一個演進機制來保證新生成的計劃不會形成性能回退。
下面咱們來看一下 OceanBase 根據自身的框架特色和業務模型如何解決查詢優化器所面臨的挑戰。
從統計信息和代價模型的維度看,OceanBase 發明了基於 LSM-TREE 存儲結構的基表訪問路徑選擇。從計劃空間的角度看,由於 OceanBase 原生就是一個分佈式關係數據庫系統,它必然要面臨的一個問題就是分佈式計劃優化。從計劃管理的角度看,OceanBase 有一整套完善的計劃管理機制。
1.基於 LSM - TREE 的基表訪問路徑選擇
基表訪問路徑選擇方法是指優化器選擇索引的方法,其本質是要評估每個索引的代價並選擇代價最小的索引來訪問數據庫中的表。對於一個索引路徑,它的代價主要由兩部分組成,掃描索引的代價和回表的代價(若是一個索引對於一個查詢來講不須要回表,那麼就沒有回表的代價)。
一般來講,索引路徑的代價取決於不少因素,好比掃描/回表的行數,投影的列數,謂詞的個數等。爲了簡化咱們的討論,在下面的分析中,咱們從行數這個維度來介紹這兩部分的代價。
在傳統關係數據庫中,掃描索引的行數和回表的行數都是經過優化器中維護的統計信息來計算謂詞選擇率獲得(或者經過一些更加高級的方法好比動態採樣)。
舉個簡單的例子,給定聯合索引(a,b)和查詢謂詞 a > 1 and a < 5 and b < 5, 那麼謂詞 a > 1 and a < 5 定義了索引掃描開始和結束的位置,若是知足這兩個條件的行數有 1w 行,那麼掃描索引的代價就是 1w 行順序掃描,若是謂詞 b < 5 的選擇率是 0.5,那麼回表的代價就是 5k 行的隨機掃描。
那麼問題來了:傳統的計算行數和代價的方法是否適合基於 LSM-TREE 的存儲引擎?
LSM-TREE 存儲引擎把數據分爲了兩部分(以下圖所示),靜態數據(基線數據)和動態數據(增量數據)。其中靜態數據不會被修改,是隻讀的,存儲於磁盤;全部的增量修改操做(增、刪、改)被記錄在動態數據中,存儲於內存。靜態數據和增量數據會按期的合併造成新的基線數據。在 LSM-TREE 存儲引擎中,對於一個查詢操做,它須要合併靜態數據和動態數據來造成最終的查詢結果。
考慮下圖中 LSM-TREE 存儲引擎基線數據被刪除的一個例子。在該圖中,基線中有 10w 行數據,增量數據中維護了對這 10w 行數據的刪除操做。在這種場景下,這張表的總行數是 0 行,在傳統的基於 Buffer-Pool 的存儲引擎上,掃描會很快,也就是說行數和代價是匹配的。可是在 LSM-TREE 存儲引擎中,掃描會很慢(10w 基線數據 + 10w 增長數據的合併),也就是行數和代價是不匹配的。
這個問題的本質緣由是在基於 LSM-TREE 的存儲引擎上,傳統的基於動態採樣和選擇率信息計算出來的行數不足以反應實際計算代價過程當中須要的行數。
舉個簡單的例子,在傳統的關係數據庫中,咱們插入 1w 行,而後刪除其中 1k 行,那麼計算代價的時候會用 9k 行去計算,在 LSM-TREE 的場景下,若是前面 1w 行是在基線數據裏面,那麼內存中會有額外的 1k 行,在計算代價的時候咱們是須要用 11k 行去計算。
爲了解決 LSM-TREE 存儲引擎的計算代價行數和表中真實行數不一致的行爲,OceanBase 提出了「邏輯行」和「物理行」的概念以及計算它們的方法。其中邏輯行能夠理解爲傳統意義上的行數,物理行主要用於刻畫 LSM-TREE 這種存儲引擎在計算代價時須要真正訪問的行數。
再考慮上圖中的例子,在該圖中,邏輯行是 0 行,而物理行是 20w 行。給定索引掃描的開始/結束位置,對於基線數據,由於 OceanBase 爲基線數據維護了塊級別的統計信息,因此能很快的計算出來基線行數。對於增量數據,則經過動態採樣方法獲取增/刪/改行數,最終二者合併就能夠獲得邏輯行和物理行。下圖展現了 OceanBase 計算邏輯行和物理行的方法。
相比於傳統的基表訪問路徑方法,OceanBase 的基於邏輯行和物理行的方法有以下兩個優點:
優點一:實時統計信息
由於同時考慮了增量數據和基線數據,至關於統計信息是實時的,而傳統方法的統計信息蒐集是有必定的滯後性的(一般是一張表的增/刪/修改操做到了必定程度,纔會觸發統計信息的從新蒐集)。
優點二:解決了索引列上的謂詞依賴關係
考慮索引(a,b)以及查詢條件 a = 1 and b = 1 , 傳統的方法在計算這個查詢條件的選擇率的時候必然要考慮的一個問題是 a 和 b 是否存在依賴關係,而後再使用對應的方法(多列直方圖或者動態採樣)來提升選擇率計算的正確率。OceanBase 目前的估行方法默認可以解決 a 和 b 的依賴關係的場景。
2.OceanBase 分佈式計劃優化
OceanBase 原生就有分佈式的屬性,那麼它必然要解決的一個問題就是分佈式計劃優化。不少人認爲分佈式計劃優化很難,無從下手,那麼分佈式計劃優化跟本地優化到底有什麼區別?分佈式計劃優化是否須要修改現有的查詢優化框架來作優化?
在筆者看來,現有的查詢優化框架徹底有能力處理分佈式計劃優化,可是分佈式計劃優化會大大增長計劃的搜索空間,主要緣由以下:
一、在分佈式場景下,選擇的是算子的分佈式算法,而算子的分佈式算法空間比算子本地算法的空間要大不少。下圖展現了一個 Hash Join 在分佈式場景下能夠選擇的分佈式算法。
二、在分佈式場景下,除了序這個物理屬性以外,還增長了分區信息這個物理屬性。分區信息主要包括如何分區以及分區的物理信息。分區信息決定了算子能夠採用何種分佈式算法。
三、在分佈式場景下,分區裁剪/並行度優化/分區內(間)並行等因素也會增大分佈式計劃的優化複雜度。
OceanBase 目前採用兩階段的方式來作分佈式優化。在第一階段,OceanBase 基於全部表都是本地的假設生成一個最優本地計劃。在第二階段,OceanBase 開始作並行優化, 用啓發式規則來選擇本地最優計劃中算子的分佈式算法。下圖展現了 OceanBase 二階段分佈式計劃的一個例子。
OceanBase 二階段的分佈式計劃優化方法能減小優化空間,下降優化複雜度,可是由於在第一階段優化的時候沒有考慮算子的分佈式信息,因此可能致使生成的計劃次優。目前 OceanBase 正在實現一階段的分佈式計劃優化:
一、在 System-R 的 Bottom-up 的動態規劃算法中,枚舉全部算子的全部分佈式實現而且維護算子的物理屬性。
二、在 System-R 的 Bottom-up 的動態規劃算法中,對於每個枚舉的子集, 保留代價最小/有 Interesting order/有 Interesting 分區的計劃。
一階段的分佈式計劃優化可能會致使計劃空間增加很快,因此必需要有一些 Pruning 規則來減小計劃空間或者跟本地優化同樣在計劃空間比較大的時候,使用遺傳算法或者啓發式規則來解決這個問題。
3.OceanBase 計劃管理機制
OceanBase 基於螞蟻/阿里真實的業務場景,構建了一套完善的計劃緩存機制和計劃演進機制。
OceanBase 計劃緩存機制
以下圖所示,OceanBase 目前使用參數化計劃緩存的方式。這裏涉及到兩個問題:爲何選擇參數化以及爲何選擇緩存?
一、參數化:在螞蟻/阿里不少真實業務場景下,爲每個參數緩存一個計劃是不切實際的。考慮一個根據訂單號來查詢訂單信息的場景,在螞蟻/阿里高併發的場景下,爲每個訂單號換成一個計劃是不切實際的,並且也不須要,由於一個帶訂單號的索引能解決全部參數的場景。
二、計劃緩存:計劃緩存是由於性能的緣由,對於螞蟻/阿里不少真實業務場景來講,若是命中計劃,那麼一個查詢的性能會在幾百 us,可是若是沒有命中計劃,那麼性能大概會在幾個 ms。對於高併發,低時延的場景,這種性能優點是很重要的。
OceanBase 使用參數化計劃緩存的方式,可是在不少螞蟻真實的業務場景下,對全部的參數使用同一個計劃並非最優的選擇。考慮一個螞蟻商戶域的業務場景,這個場景以商戶的維度去記錄每一筆帳單信息,商戶能夠根據這些信息作一些分析和查詢。這種場景確定會存在大小帳號問題,以下圖所示,淘寶可能貢獻了 50% 的訂單,LV 可能只貢獻了 0.1% 的訂單。考慮查詢「統計一個商戶過去一年的銷售額」,若是是淘寶和美團這種大商戶,那麼直接主表掃描會是一個合理的計劃,對於 LV 這種小商戶,那麼走索引會是一個合理的計劃。
爲了解決不一樣參數對應不一樣計劃的問題,OceanBase 實現了以下圖所示的自適應計劃匹配。該方法會經過直方圖和執行反饋來監控每個緩存的計劃是否存在不一樣參數須要對應不一樣計劃的問題。一旦存在,自適應計劃匹配會經過漸進式的合併選擇率空間來達到把整個選擇率空間劃分紅若干個計劃空間(每一個空間對應一個計劃)的目的。
OceanBase 計劃演進機制
在螞蟻/阿里不少高併發,低時延的業務場景下,OceanBase 必需要保證新生成的計劃不會致使性能回退。下圖展現了 OceanBase 對新計劃的演進過程。不一樣於傳統的數據庫系統採用定時任務和後臺進程演進的方式,OceanBase 會使用真實的流量來進行演進,這樣的一個好處是能夠及時的更新比較優的計劃。好比當業務新建了一個更優的索引時,傳統數據庫系統並不能馬上使用該索引,須要在演進定時任務啓動後才能演進驗證使用,而 OceanBase 能夠及時的使用該計劃。
OceanBase 查詢優化器的實現立足於自身架構和業務場景特色,好比 LSM-TREE 存儲結構、Share-Nothing 的分佈式架構和大規模的運維穩定性。OceanBase 致力於打造基於 OLTP 和 OLAP 融合的查詢優化器。從 OLTP 的角度看,咱們立足於螞蟻/阿里真實業務場景,完美承載了業務需求。從 OLAP 的角度看,咱們對標商業數據庫,進一步打磨咱們 HTAP 的優化器能力。
原文連接 本文爲雲棲社區原創內容,未經容許不得轉載。