綜述
由於SQL是宣告式語言(declarative),使用者只告訴了DBMS想要獲取什麼,但沒有指出如何計算。因此,DBMS需要將SQL語句轉換成可執行的查詢計劃(Query Plan)。但是對同樣的資料可以有多種查詢方案,效能也差距很大,查詢優化器(Query Optimizer)的任務就是從給定的查詢中選擇一個最優的方案。
最早的查詢優化器實現是IBM在1970s設計的 System R,其中的概念和設計到現在依然有很多使用。對於查詢優化通常有兩種方案:
- 基於啟發式規則:啟發式優化將查詢的部分與已知的模式進行匹配,以重組計劃。這些規則對查詢進行轉換,消除低效率的部分,這種方式不需要檢查資料本身。
- 基於代價的搜尋:需要讀取資料並估計執行計劃的成本。然後從各個計劃中選擇成本最低的方案。
等價的關係代數(啟發式規則)
如果兩個關係代數表示式生成相同的元組集,則它們是等價的。DBMS 可以在沒有成本模型的條件下生成更優的查詢計劃,即查詢重寫(Query Rewriting)
當應用程式向資料庫傳送SQL查詢,DBMS首先要將SQL解析成語法樹的標記,Binder 查詢系統目錄將語法樹標記替換為內部識別符號,生成邏輯查詢計劃,最後由查詢優化器選擇最高效的執行方案。
一種等價的關係代數是謂詞下推,對於這樣的SQL查詢:
SELECT s.name, e.cid
FROM student AS s, enrolled AS e
WHERE s.sid = e.sid
AND e.grade = 'A'
相比於先連線再過濾,應當更早地對資料進行過濾,以減少連線時的元素數量。
有關選擇(selection)的優化:
- 儘早執行過濾
- 重排謂詞,將最具選擇性的謂詞優先應用
- 分解複雜的謂詞,將之往下推
有關投影(projection)的優化:(列儲存無需進行這兩條優化)
- 儘早進行投影以建立更小的元組並減少中間結果
- 只投影被需要的屬性
有關連線(join)的優化:
-
R⋈S = S⋈R,因此可以重排多個表的連線順序
-
但對於n個表,不同的連線順序為卡特蘭數(\(≈4^n\))
如果要對所有順序窮舉的話,當n較大時效率會非常低。連線順序通常由cost based search選擇最優/較優的方案。
SELECT ARTIST.NAME
FROM ARTIST, APPEARS, ALBUM
WHERE ARTIST.ID=APPEARS.ARTIST_ID
AND APPEARS.ALBUM_ID=ALBUM.ID
AND ALBUM.NAME="Andy's OG Remix"
對於這樣的SQL查詢,最樸素的查詢方案可能是左圖所示,但通過:
- 分解複雜謂詞並向下推
- 將笛卡爾積替換為連線
- 在連線前消除不必要的屬性
可以優化為右圖所示的方案
其他優化包括:
忽略不必要的join、projection
SELECT A1.*
FROM A AS A1 JOIN A AS A2
ON A1.id = A2.id; //unnecessary
SELECT * FROM A AS A1
WHERE EXISTS(SELECT val FROM A AS A2 //unnecessary
WHERE A1.id = A2.id);
合併謂詞:
SELECT * FROM A
WHERE val BETWEEN 1 AND 100
OR val BETWEEN 50 AND 150;
對於巢狀查詢,有兩種方案:
- 重寫,將其轉化為單次查詢
- 先進行子查詢,將結果儲存在臨時表中。得出最終結果後將臨時表丟棄。
基於代價的搜尋
這種優化方式分為兩個步驟:
- 成本估計
- 方案選擇
成本估計
首先要為特定的執行計劃生成成本估算,而訪問磁碟的消耗始終是查詢中最主要的消耗,並且還要考慮順序訪問還是隨機訪問,這兩者在效能上也有極大差異。
選擇基數
DBMS 在目錄中儲存有關屬性、索引的內部資訊。對於每個關係R,DBMS維護以下資訊:
- \(N_R\) :R中的元組數量
- \(V(A, R)\):R中在屬性A上不同值的數目
則選擇基數(selection cardinality SC(A,R))為給定屬性的值的平均數量 \(SC(A, R) = N_R / V(A,R)\)
在計算cost的時候,需要考慮不同謂詞選擇的範圍。謂詞的選擇性(selective)即是一個謂詞限定的部分。
比如對SC(A,R)=2的關係R中,若A的資料為1-100的連續整數,則對於查詢
SELECT * FROM R
WHERE A >50
可以計算出\(sel(A>50) = 50/100 = 1/2\)
SELECT * FROM R
WHERE A = 2
OR B LIKE 'A%'
\(sel(P1 ⋁ P2) = sel(P1) + sel(P2) – sel(P1⋀P2) = sel(P1) + sel(P2) – sel(P1) ∙ sel(P2)\)
也可以說,選擇性就是指這個範圍的資料出現的概率。
但以上的估計基於三個假設:
- 資料是均勻分佈的
- 多個謂詞之間相互獨立,可以獨立計算概率
- 內部關係中的key在外表中同樣存在
因此得出的結果是一個估計值,並不精確。
統計直方圖
可以對第一個假設進行優化,在每個表中儲存有關資料的直方圖,將資料按範圍進行統計,在計算sel時從直方圖中計算相應的比例。
樣本估算
現代DBMS從表中選擇一定的樣本估算sel,當底層表發生顯著變化時更新樣本。
方案選擇
對於簡單的單表查詢(OLTP),通過啟發式規則,利用索引和二分搜尋足以獲取良好的效能。
但是對於OLAP中的多表查詢,不同的連線順序會對效能造成很大影響。而由於關係的增加會導致可選擇方案指數增長(\(4^n\)),因此需要約束可選擇的空間。
System R 中只考慮左深連線樹(Left Deep Join),將選擇空間縮小到 \(n!\),但現代DBMS中不再總做出這樣的假設。
左深連線樹即連線的右表一定為一個基本表,通過流水線連線,中間結果不寫入臨時檔案。
對於連線,需要考慮連線的順序,不同表之間連線的方式(Hash join, Sort-Merge join),獲取資料的方式。通過動態規劃對方案進行剪枝。
除了通過動態規劃剪枝之外,當連線表過多時,會選擇一些區域性最優解的方式:
-
greedy join enumeration algorithm
在每次迴圈中,選擇使總代價最低的方案
- 多項式時間演算法,但結果不一定最優
-
Randomized algorithm
隨機重寫查詢方案,利用模擬退火等演算法進行優化
-
Genetic algorithm(遺傳演算法)
通過連線方案(結合子代)和隨機突變進行優化
至於為什麼是左深連線樹,而不是右深連線樹?動態規劃的執行優化又是如何實現的?留待後面分析