綜述
由於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(遺傳算法)
通過連接方案(結合子代)和隨機突變進行優化
至於為什么是左深連接樹,而不是右深連接樹?動態規划的執行優化又是如何實現的?留待后面分析