1. 程式人生 > >postgresql 技術內幕學習筆記

postgresql 技術內幕學習筆記

本文原文地址(讀了兩篇文章的重點筆記):
PostgreSQL查詢優化器詳解之邏輯優化篇
PostgreSQL查詢優化器詳解之物理優化篇

第一章 概述

1.1 查詢優化的簡介

一個查詢優化器它的輸入是查詢樹,輸出是查詢執行計劃 。

通常資料庫的查詢優化分為兩個層次:

  • 基於規則的查詢優化(邏輯優化:rule based optimization)
  • 基於代價的查詢優化 (物理優化,cost based optimization)

查詢樹就是查詢優化器的輸入,經過邏輯優化和物理優化,最終產生一顆最優的計劃樹


1.2 邏輯優化篇

PostgreSQL在邏輯優化階段有這麼幾個重要的優化:

子查詢&子連線提升、表示式預處理、外連線消除、謂詞下推、連線順序交換、等價類推理

PostgreSQL的子查詢和子連線
出現在FROM關鍵字後的子句是子查詢語句,出現在WHERE/ON等約束條件中或投影中的子句是子連線語句

SELECT * FROM STUDENT, (SELECT * FROM SCORE) as sc;

SELECT (SELECT AVG(degree) FROM SCORE), sname FROM STUDENT;

SELECT * FROM STUDENT WHERE EXISTS (SELECT A FROM SCORE WHERE SCORE.
sno = STUDENT.sno);

1是子查詢,2和3是子連線,語句1裡的子句出現在FROM後面,它是以‘表’的形式存在的,是子查詢,2和3的子句出現在投影和約束條件中,是以表示式的形式存在的,是子連線。

子查詢 還可以分為相關子連線和非相關子連線,相關子連線是指在子查詢語句中引用了外層表的列屬性,這就導致外層表每獲得一個元組,子查詢就需要重新執行一次

而非相關子查詢是指在子查詢語句是獨立的,和外層的表沒有直接的關聯,子查詢可以單獨執行一次,外層表可以重複利用子查詢的執行結果

PostgreSQL提升了兩種型別的子連線,一種是ANY型別的子連線,一種是EXISTS型別的子連線,對於ANY型別的子連線,只提升非相關子連線,而對於EXISTS型別的子連線,則只提升相關子連線

雖然PostgreSQL對於ANY型別只提升非相關的子連線但它仍然是隻提升產生巢狀迴圈的那種子連線,看這個例子

SELECT * FROM STUDENT WHERE sno > ANY (SELECT sno from STUDENT);

這是一個ANY型別的非相關子連線,但請注意,在>前面的sno實際上產生了一個天然的相關性,這個天然的相關性就會產生巢狀迴圈,因此是需要提升的。

-- 把>前面的sno換成了一個常量
SELECT * FROM STUDENT WHERE 10 > ANY (SELECT sno from STUDENT);

這個SQL語句中的子連線就不會提升了,因為我們把sno換成了常量,父子之間的相關性被打破


子連線是否提升取決於相關性,而這個相關性不只是體現在子句裡,也體現在表示式裡;只要能產生巢狀迴圈,那就有提升的必要。 ANY型別的相關子連線也會產生巢狀迴圈,卻不提升。

PostgreSQL提升ANY型別的子連線的方式和EXISTS型別的子連線的方式不同,他提升EXISTS型別的子連線的時候,是直接把子句中的表提上來做,形成一個SemiJoin,可是提升ANY型別的子連線時,是把整個子句提上來,和父語句中的表做SemiJoin,這時候這個子句就變成了一個子查詢

SELECT * FROM TEST_A WHERE a > ANY (SELECT a FROM TEST_B WHERE TEST_A.b = TEST_B.b)SELECT * FROM TEST_A, (SELECT a FROM TEST_B WHERE TEST_A.b = TEST_B.b) b WHERE TEST_A.a > b.a;

SELECT * FROM TEST_A, LATERAL (SELECT a FROM TEST_B WHERE TEST_A.b = TEST_B.b) b WHERE TEST_A.a > b.a;

如果按照目前ANY型別子連線先提升成子查詢的方式,第1個語句提升之後會變成等價於第2個語句,而第2個語句本身是無法執行的,在比較新版本的PostgreSQL上支援了LATERAL之後,只要在第2個語句上加上LATERAL,也就是變成第3個語句就能執行了


1.3 關於選擇下推與等價類

選擇下推是為了儘早地過濾資料,這樣就能在上層結點降低計算量


二、PostgreSQL查詢優化器詳解之物理優化

2.1 關於統計資訊與選擇率

PostgreSQL的物理優化需要計算各種物理路徑的代價,而代價估算的過程嚴重依賴於資料庫的統計資訊,統計資訊是否能準確地描述表中的資料分佈情況是決定代價準確性的重要條件之一

-- 這兩個語句可以用同樣的物理運算元來完成,但是他們的計算量一樣嗎?
SELECT A+B FROM TEST_A WHERE A > 1;
SELECT A+B FROM TEST_A WHERE A > 100000000;

A > 1和A > 1000000000都是過濾條件,經過過濾之後,他們產生的資料量就不同了,這樣投影中的A+B的計算次數就不同了;要知道A > 1之後還剩下多少資料或者A > 1000000000之後還剩下多少資料,如果我們提前對錶上的資料內容做了統計,剩下多少資料就不難計算了,所以必須要有統計資訊。

通過統計資訊,代價估算系統就可以瞭解一個表有多少行資料、用了多少個數據頁面、某個值出現的頻率等等,然後就能根據這些資訊計算出一個約束條件能過濾掉多少資料,這種約束條件過濾出的資料佔總資料量的比例稱之為‘選擇率’,所謂選擇率就是一個比例
在這裡插入圖片描述
當實際上比上面的更復雜。


2.2 關於物理路徑

  • 掃描路徑有順序掃描路徑、索引掃描路徑、點陣圖掃描路徑等等;
  • 而連線路徑通常有巢狀迴圈連線路徑、雜湊連線路徑、歸併連線路徑,另外還有一些其他的路徑,比如排序路徑、物化路徑等等

2.2.1 順序掃描

如果要獲得一個表中的資料,最基礎的方法就是將表中的所有的資料都遍歷一遍,從中挑選出符合條件的資料,這種方式就是順序掃描路徑

順序掃描路徑的優點是具有廣泛的適用性,各種表都可以用這種方法,缺點自然是代價通常比較高,因為要把所有的資料都遍歷一遍

在這裡插入圖片描述


2.2.2 索引掃描

如果將資料做一些預處理,比如建立一個索引,如果要想獲得一個表的資料,可以通過掃描索引獲得所需資料的‘地址’,然後通過地址將需要的資料獲取出來。尤其是在選擇操作帶有約束條件的情況下,在索引和約束條件共同的作用下,表中有些資料就不用再遍歷了,因為通過索引就很容易知道這些資料是不符合約束條件的,更有甚者,因為索引上也儲存了資料,它的資料和關係中的資料是一致的,因此如果索引上的資料就能滿足要求,只需要掃描索引就可以獲得所需資料了,也就是說在掃描路徑中還可以有索引掃描路徑和快速索引掃描路徑兩種方式

在這裡插入圖片描述

這個索引掃描有隨機讀的問題,這個問題能否解決掉呢?也就是說即利用了索引,還避免了隨機讀的問題,有這樣的辦法嗎

索引掃描路徑確實帶來隨機讀的問題,因為索引中記錄的是資料元組的地址,索引掃描是通過掃描索引獲得元組地址,然後通過元組地址訪問資料,索引中儲存的“有序”的地址,到資料中就可能是隨機的了。點陣圖掃描就能解決這個問題,它通過點陣圖將地址儲存起來,把地址收集起來之後,然後讓地址變得有序,這樣就通過中間的點陣圖把隨機讀消解掉了

在這裡插入圖片描述

掃描過程中還會結合一些特殊的情況有一些非常高效的掃描路徑,比如TID掃描路徑,TID實際上是元組在磁碟上的儲存地址,我們能夠根據TID直接就獲得元組,這樣查詢效率就非常高了


2.2.3 Nestlooped Join

掃描路徑通常是執行計劃中的葉子結點,也就是在最底層對錶進行掃描的結點,掃描路徑就是為連線路徑做準備的,掃描出來的資料就可以給連線路徑來實現連線操作了

要對兩個關係做連線,受笛卡爾積的啟發,可以用一個演算法複雜度是O(mn)的方法來實現,我們叫它Nestlooped Join方法。這種方法雖然複雜度比較高,但是和順序掃描一樣,勝在具有普適性

巢狀迴圈連線這種方法的時間複雜度比較高,看上去沒什麼意義,但是如果Nestlooped Join內表的路徑是一個索引掃描路徑,那麼演算法的複雜度就會降下來。索引掃描的演算法複雜度是O(logn),因此如果Nestlooped Join的內表是一個索引掃描,它的整體的演算法複雜度就變成了O(mlogn),看上去這樣也是可以接受的

在這裡插入圖片描述


2.2.3 Hash Join

假設Hash表有N個桶,內表資料均勻的分佈在各個桶中,那麼Hash Join的時間複雜度就是O(m * n /N),當然,這裡我們沒有考慮上建立Hash表的代價;Hash連線通常只能用來做等值判斷

在這裡插入圖片描述


2.2.4 Merge Join

如果將兩個表先排序,那麼就可以引入第三種連線方式,Merge Join。這種連線方式的代價主要浪費在排序上,如果兩個關係的資料量都比較小,那麼排序的代價是可控的,MergeJoin就是適用的。另外如果關係上有有序的索引,那就可以不用單獨排序了,這樣也比較適用於MergeJoin

如下圖:外表是需要排序的,而內表則借用了原有的索引的順序,消除了排序的時間,降低了物理路徑的代價
在這裡插入圖片描述
這些路徑屬於SPJ路徑,在PostgreSQL的優化器中,通常會先生成SPJ的路徑,然後在這基礎上再疊加Non-SPJ的路徑,比如說聚集操作、排序操作、limit操作、分組操作


2.3 關於代價的計算

雖然在代價估算的過程中,我們無法獲得‘絕對真實’的代價,但是‘絕對真實’的代價也是不必要的。因為我們只是想從多個路徑(Path)中找到一個代價最小的路徑,只要這些路徑的代價是可以‘相互比較’的就可以了。因此可以設定一個‘相對’的代價的單位1,同一個查詢中所有的物理路徑都基於這個“相對”的單位1來計算的代價,這樣計算出來的代價就是可以比較的,也就能用來對路徑進行挑選了

PostgreSQL採用順序讀寫一個頁面的IO代價作為單位1,而把隨機IO定為了順序IO的4倍

目前的儲存介質很大部分仍然是機械硬碟,機械硬碟的磁頭在獲得資料庫的時候需要付出尋道時間。如果要讀寫的是一串在磁碟上連續的資料,就可以節省尋道時間,提高IO效能。而如果隨機讀寫磁碟上任意扇區的資料,那麼會有大量的時間浪費在尋道上。其次,大部分磁碟本身帶有快取,這就形成了主存→磁碟快取→磁碟的三級結構。在將磁碟的內容載入到記憶體的時候,考慮到磁碟的IO效能,磁碟會進行資料的預讀,把預讀到的資料儲存在磁碟的快取中。也就是說如果使用者只打算從磁碟讀取100個位元組的資料,那麼磁碟可能會連續地讀取磁碟中的512位元組(不同的磁碟預讀的數量可能不同)並將其儲存到磁碟快取。如果下一次是順序讀取100個位元組之後的內容,那麼預讀的512位元組的資料就會發揮作用,效能會大大的增加。而如果讀取的內容超出了512位元組的範圍,那麼預讀的資料就沒有發揮作用,磁碟的IO效能就會下降

目前PostgreSQL的查詢優化大量的考慮了隨機IO和順序IO所帶來的效能差別,在這方面做了不少優化。但是現在的磁碟技術越來越發達了,以後隨機IO和順序IO是不是還差這麼多,就值得商榷了


2.3.1 基準代價

在實際應用中,資料庫使用者的配置硬體環境千差萬別,CPU的頻率、主存的大小和磁碟介質的性質都會影響執行計劃在實際執行時的效率


基於磁碟IO的代價單位當然就是和Page有關的了,也就是說我們剛才說的順序IO和隨機IO都屬於IO方面的基準代價

CPU方面的基準單位有哪些呢?比如說我們通過IO把磁碟頁面讀到了快取,但我們要處理的是元組啊,所以還需要把元組從頁面裡解出來,還要處理元組,這部分主要消耗的是CPU,所以會有一個元組 處理的代價基準單位 。另外,我們在投影、約束條件裡有大量的表示式,這些表示式求解也主要消耗CPU資源,所以還有一個 表示式代價的基準單位

現在PostgreSQL增加了很多並行路徑,因此它也產生了通訊代價,這個也需要計算的

總代價 = CPU代價 + IO代價 + 通訊代價


通過EXPLAIN還檢視過PostgreSQL的執行計劃,從執行計劃中還看到有啟動代價和總代價

這是從另一個角度來計算代價,啟動代價是指從語句開始執行到查詢引擎返回第一條元組的代價(另一種說法是準備好去獲得第一條元組的代價),總代價是SQL語句從開始執行到結束的所有代價

總代價 = 啟動代價 + 執行代價


2.3.2 為什麼要區分啟動代價和執行代價

SELECT * FROM TEST_A WHERE a > 1 ORDER BY a LIMIT 1;

假設這個在TEST_A(a)上有一個B樹索引,那這個語句可能會形成什麼樣的執行計劃呢

執行路徑1:LIMIT 1
		    	-> SORT(a)
		             -> SeqScan WHERE A > 1;
執行路徑2:LIMIT 1
              	 -> IndexScan WHERE A > 1; 
                  	注:B樹索引有序,不用再排序了)

PostgreSQL採用動態規劃的方法來實現路徑的搜尋,它是一種自底向上的方法,也就是說會先建立篩選掃描路徑,然後用篩選後的掃描路徑再去形成連線路徑。那麼在我們篩選掃描路徑的時候,是不知道它的上層有沒有LIMIT的,這時候如果單獨看SeqScan + SORTIndexScan哪個好呢

A > 1的選擇率高的話會選擇順序掃描,而A > 1的選擇率低的情況下,會選擇索引掃描。這是因為索引掃描會產生隨機IO,也就是說在選擇率高的情況下,有可能SeqScan + SORT會優於IndexScan。雖然SeqScan + SORT會有排序,但是IndexScan的隨機IO實在是太可觀

假設選擇率比較高,這時選擇了SeqScan + SORT,是因為它不知道再上層是LIMIT 1。如果上面是LIMIT 1,就會導致索引掃描不用全部掃完,只要掃一丟丟就可以了。這時隨機IO就很小了,但是SeqScan + SORT就還必須全部執行完才能獲取到LIMIT 1,也就是說SeqScan + SORT、或者說SORT要獲取第一條元組的啟動代價是比較高的。如果上面有LIMIT 1這樣的子句,那麼啟動代價高的路徑可能就沒有優勢了,這就是啟動代價的作用

SORT要全部做完才能獲取第一條元組,它的啟動代價大,但是總代價小。而索引掃描呢,因為本身有序,它的啟動代價是小的,但是由於有隨機IO,所以它的總代價是大的


2.4 關於最優路徑

例如在掃描路徑中,我們就可以有順序掃描、索引掃描和點陣圖掃描。假如一個表上有多個索引,就可能產生多個不同的索引掃描,那麼哪個索引掃描路徑好呢?還有索引掃描和順序掃描、點陣圖掃描相比,哪個好呢

資料庫路徑的搜尋方法通常有3種類型:自底向上方法、自頂向下方法、隨機方法,而PostgreSQL採用了其中的兩種方法。

自底向上和隨機方法,其中自底向上的方法是採用動態規劃方法,而隨機方法採用的是遺傳演算法;
Pivotal公司的開源優化器ORCA用的就是自頂向下的方法


參考

PostgreSQL查詢優化器詳解之邏輯優化篇
PostgreSQL查詢優化器詳解之物理優化篇