下面展示的示例使用了 PostgreSQL 迴歸測試資料庫中的表。另請注意,由於 ANALYZE
在生成統計資訊時使用隨機抽樣,因此在每次新的 ANALYZE
之後,結果會略有變化。
讓我們從一個非常簡單的查詢開始
EXPLAIN SELECT * FROM tenk1; QUERY PLAN ------------------------------------------------------------- Seq Scan on tenk1 (cost=0.00..458.00 rows=10000 width=244)
規劃器如何確定 tenk1
的基數在 第 14.2 節 中有介紹,但為了完整性,這裡再重複一遍。頁數和行數從 pg_class
中查詢
SELECT relpages, reltuples FROM pg_class WHERE relname = 'tenk1'; relpages | reltuples ----------+----------- 358 | 10000
這些數字是截至上次對錶執行 VACUUM
或 ANALYZE
時的最新資料。然後,規劃器獲取表中實際的當前頁數(這是一個廉價的操作,不需要掃描表)。如果這個值與 relpages
不同,那麼 reltuples
將會相應地進行縮放,以得出一個當前的行數估計值。在上面的例子中,relpages
的值是最新的,所以行數估計值與 reltuples
相同。
接下來看一個 WHERE
子句中帶有範圍條件的示例
EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 1000; QUERY PLAN -------------------------------------------------------------------------------- Bitmap Heap Scan on tenk1 (cost=24.06..394.64 rows=1007 width=244) Recheck Cond: (unique1 < 1000) -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..23.80 rows=1007 width=0) Index Cond: (unique1 < 1000)
規劃器會檢查 WHERE
子句的條件,並在 pg_operator
中查詢運算子 <
的選擇性函式。該函式儲存在 oprrest
列中,在本例中是 scalarltsel
。scalarltsel
函式從 pg_statistic
中檢索 unique1
的直方圖。對於手動查詢,檢視更簡單的 pg_stats
檢視會更方便
SELECT histogram_bounds FROM pg_stats WHERE tablename='tenk1' AND attname='unique1'; histogram_bounds ------------------------------------------------------ {0,993,1997,3050,4040,5036,5957,7057,8029,9016,9995}
接下來,計算出“< 1000”在直方圖中所佔的比例。這就是選擇性。直方圖將範圍劃分為等頻桶,所以我們只需找到我們的值所在的桶,並計算它所佔的部分以及它之前的所有桶。值 1000 顯然在第二個桶 (993–1997) 中。假設每個桶內的值是線性分佈的,我們可以這樣計算選擇性:
selectivity = (1 + (1000 - bucket[2].min)/(bucket[2].max - bucket[2].min))/num_buckets = (1 + (1000 - 993)/(1997 - 993))/10 = 0.100697
也就是說,一個完整的桶加上第二個桶的線性部分,再除以桶的總數。現在,估計的行數可以透過選擇性與 tenk1
的基數的乘積來計算
rows = rel_cardinality * selectivity = 10000 * 0.100697 = 1007 (rounding off)
接下來,讓我們考慮一個 WHERE
子句中帶有相等條件的示例
EXPLAIN SELECT * FROM tenk1 WHERE stringu1 = 'CRAAAA'; QUERY PLAN ---------------------------------------------------------- Seq Scan on tenk1 (cost=0.00..483.00 rows=30 width=244) Filter: (stringu1 = 'CRAAAA'::name)
規劃器再次檢查 WHERE
子句的條件,並查詢 =
的選擇性函式,即 eqsel
。對於相等性估計,直方圖沒有用;而是使用最常見值(MCVMCV)列表來確定選擇性。讓我們看一下 MCV,以及一些後面會用到的額外列
SELECT null_frac, n_distinct, most_common_vals, most_common_freqs FROM pg_stats WHERE tablename='tenk1' AND attname='stringu1'; null_frac | 0 n_distinct | 676 most_common_vals | {EJAAAA,BBAAAA,CRAAAA,FCAAAA,FEAAAA,GSAAAA,JOAAAA,MCAAAA,NAAAAA,WGAAAA} most_common_freqs | {0.00333333,0.003,0.003,0.003,0.003,0.003,0.003,0.003,0.003,0.003}
由於 CRAAAA
出現在 MCV 列表中,所以選擇性就是最常見頻率(MCF)列表中對應的條目
selectivity = mcf[3] = 0.003
和之前一樣,估計的行數就是這個選擇性與 tenk1
的基數的乘積
rows = 10000 * 0.003 = 30
現在考慮相同的查詢,但常量不在 MCVMCV列表中
EXPLAIN SELECT * FROM tenk1 WHERE stringu1 = 'xxx'; QUERY PLAN ---------------------------------------------------------- Seq Scan on tenk1 (cost=0.00..483.00 rows=15 width=244) Filter: (stringu1 = 'xxx'::name)
這是一個完全不同的問題:當值不在 MCVMCV列表中時,如何估計選擇性。方法是利用該值不在列表中的事實,並結合所有 MCVMCVs
selectivity = (1 - sum(mcv_freqs))/(num_distinct - num_mcv) = (1 - (0.00333333 + 0.003 + 0.003 + 0.003 + 0.003 + 0.003 + 0.003 + 0.003 + 0.003 + 0.003))/(676 - 10) = 0.0014559
的頻率知識。具體做法是,將所有 MCVMCV的頻率加起來,用 1 減去這個和,然後除以其他不同值的數量。這相當於假設,不屬於任何 MCV 的那部分列值在所有其他不同值之間是均勻分佈的。請注意,這裡沒有空值,所以我們不必擔心它們(否則我們也要從分子中減去空值比例)。然後像往常一樣計算估計的行數
rows = 10000 * 0.0014559 = 15 (rounding off)
前面 unique1 < 1000
的例子是對 scalarltsel
實際工作的過度簡化;現在我們已經看到了使用 MCV 的例子,可以補充一些細節。那個例子就其本身而言是正確的,因為 unique1
是一個唯一列,所以它沒有 MCV(顯然,沒有哪個值比其他任何值更常見)。對於一個非唯一列,通常既有直方圖,也有 MCV 列表,並且直方圖不包含由 MCV 代表的那部分列總體。我們這樣做是因為它能提供更精確的估計。在這種情況下,scalarltsel
會直接將條件(例如,“< 1000”)應用於 MCV 列表中的每個值,並累加那些滿足條件的 MCV 的頻率。這為表中屬於 MCV 的那部分提供了一個精確的選擇性估計。然後,直方圖會像上面那樣用於估計不屬於 MCV 的那部分表中的選擇性,最後將這兩個數字結合起來,估算出總的選擇性。例如,考慮
EXPLAIN SELECT * FROM tenk1 WHERE stringu1 < 'IAAAAA'; QUERY PLAN ------------------------------------------------------------ Seq Scan on tenk1 (cost=0.00..483.00 rows=3077 width=244) Filter: (stringu1 < 'IAAAAA'::name)
我們已經看到了 stringu1
的 MCV 資訊,下面是它的直方圖
SELECT histogram_bounds FROM pg_stats WHERE tablename='tenk1' AND attname='stringu1'; histogram_bounds -------------------------------------------------------------------------------- {AAAAAA,CQAAAA,FRAAAA,IBAAAA,KRAAAA,NFAAAA,PSAAAA,SGAAAA,VAAAAA,XLAAAA,ZZAAAA}
檢查 MCV 列表,我們發現條件 stringu1 < 'IAAAAA'
被前六個條目滿足,而不被後四個滿足,因此在總體中 MCV 部分的選擇性是
selectivity = sum(relevant mvfs) = 0.00333333 + 0.003 + 0.003 + 0.003 + 0.003 + 0.003 = 0.01833333
將所有 MCF 相加也告訴我們,由 MCV 代表的總人口比例是 0.03033333,因此由直方圖代表的比例是 0.96966667(同樣,沒有空值,否則我們也必須在這裡排除它們)。我們可以看到,值 IAAAAA
幾乎落在第三個直方圖桶的末尾。透過一些相當粗略的關於不同字元頻率的假設,規劃器得出的估計是,直方圖總體中小於 IAAAAA
的部分為 0.298387。然後我們結合 MCV 和非 MCV 總體的估計值
selectivity = mcv_selectivity + histogram_selectivity * histogram_fraction = 0.01833333 + 0.298387 * 0.96966667 = 0.307669 rows = 10000 * 0.307669 = 3077 (rounding off)
在這個特定的例子中,來自 MCV 列表的修正相當小,因為列的分佈實際上非常平坦(統計資料顯示這些特定值比其他值更常見,主要是由於抽樣誤差)。在一個更典型的情況下,即某些值明顯比其他值更常見時,這個複雜的過程會顯著提高準確性,因為最常見值的選擇性是被精確計算出來的。
現在讓我們考慮一個 WHERE
子句中有多個條件的情況
EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 1000 AND stringu1 = 'xxx'; QUERY PLAN -------------------------------------------------------------------------------- Bitmap Heap Scan on tenk1 (cost=23.80..396.91 rows=1 width=244) Recheck Cond: (unique1 < 1000) Filter: (stringu1 = 'xxx'::name) -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..23.80 rows=1007 width=0) Index Cond: (unique1 < 1000)
規劃器假設這兩個條件是獨立的,因此各個子句的選擇性可以相乘
selectivity = selectivity(unique1 < 1000) * selectivity(stringu1 = 'xxx') = 0.100697 * 0.0014559 = 0.0001466 rows = 10000 * 0.0001466 = 1 (rounding off)
請注意,估計從點陣圖索引掃描返回的行數僅反映了與索引一起使用的條件;這很重要,因為它會影響後續堆獲取的成本估計。
最後,我們將研究一個涉及連線的查詢
EXPLAIN SELECT * FROM tenk1 t1, tenk2 t2 WHERE t1.unique1 < 50 AND t1.unique2 = t2.unique2; QUERY PLAN -------------------------------------------------------------------------------------- Nested Loop (cost=4.64..456.23 rows=50 width=488) -> Bitmap Heap Scan on tenk1 t1 (cost=4.64..142.17 rows=50 width=244) Recheck Cond: (unique1 < 50) -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..4.63 rows=50 width=0) Index Cond: (unique1 < 50) -> Index Scan using tenk2_unique2 on tenk2 t2 (cost=0.00..6.27 rows=1 width=244) Index Cond: (unique2 = t1.unique2)
對 tenk1
的限制條件 unique1 < 50
在巢狀迴圈連線之前進行評估。這與前面的範圍示例類似。這一次,值 50 落在了 unique1
直方圖的第一個桶中
selectivity = (0 + (50 - bucket[1].min)/(bucket[1].max - bucket[1].min))/num_buckets = (0 + (50 - 0)/(993 - 0))/10 = 0.005035 rows = 10000 * 0.005035 = 50 (rounding off)
連線的限制條件是 t2.unique2 = t1.unique2
。運算子是我們熟悉的 =
,但是選擇性函式是從 pg_operator
的 oprjoin
列中獲取的,即 eqjoinsel
。eqjoinsel
會查詢 tenk2
和 tenk1
的統計資訊
SELECT tablename, null_frac,n_distinct, most_common_vals FROM pg_stats WHERE tablename IN ('tenk1', 'tenk2') AND attname='unique2'; tablename | null_frac | n_distinct | most_common_vals -----------+-----------+------------+------------------ tenk1 | 0 | -1 | tenk2 | 0 | -1 |
在這種情況下,unique2
沒有 MCVMCV資訊,並且所有值似乎都是唯一的(n_distinct = -1),所以我們使用一個依賴於兩個關係行數估計值(num_rows,未顯示,但為“tenk”)以及列的空值比例(兩者都為零)的演算法
selectivity = (1 - null_frac1) * (1 - null_frac2) / max(num_rows1, num_rows2) = (1 - 0) * (1 - 0) / max(10000, 10000) = 0.0001
即,從每個關係的 1 中減去空值比例,然後除以較大關係的行數(在非唯一情況下,該值會被縮放)。連線可能產生的行數計算為兩個輸入的笛卡爾積的基數乘以選擇性
rows = (outer_cardinality * inner_cardinality) * selectivity = (50 * 10000) * 0.0001 = 50
如果這兩列有 MCV 列表,eqjoinsel
將會使用直接比較 MCV 列表的方式來確定由 MCV 代表的那部分列總體中的連線選擇性。對於總體中其餘部分的估計則遵循這裡展示的相同方法。
注意,我們展示的 inner_cardinality
是 10000,也就是 tenk2
未經修改的大小。從 EXPLAIN
的輸出中看,連線行的估計值似乎來自於 50 * 1,即外部行的數量乘以每次對 tenk2
進行內部索引掃描所獲得的估計行數。但事實並非如此:連線關係的大小是在考慮任何特定連線計劃之前估算的。如果一切正常,那麼這兩種估算連線大小的方法會產生大致相同的答案,但由於舍入誤差和其他因素,它們有時會顯著不同。
對於對更多細節感興趣的人,表大小的估計(在任何 WHERE
子句之前)是在 src/backend/optimizer/util/plancat.c
中完成的。子句選擇性的通用邏輯在 src/backend/optimizer/path/clausesel.c
中。特定於運算子的選擇性函式主要在 src/backend/utils/adt/selfuncs.c
中找到。
如果您在文件中發現任何不正確、與您對特定功能的體驗不符或需要進一步澄清的內容,請使用此表單報告文件問題。