以下範例使用 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
。對於相等估計,直方圖沒有用;而是使用最常見的值 (MCVs)清單來確定選擇性。讓我們看看 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 清單中,因此選擇性只是最常見頻率(MCFs)清單中的相應條目
selectivity = mcf[3] = 0.003
與之前一樣,估計的列數只是這個值與 tenk1
基數的乘積
rows = 10000 * 0.003 = 30
現在考慮相同的查詢,但使用不在MCV清單
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)
中的常數MCV這是一個非常不同的問題:當值不在MCV清單
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
rows = 10000 * 0.0014559 = 15 (rounding off)
的頻率知識。也就是說,將所有 MCV 的頻率加總,並從 1 中減去,然後除以其他 不同值的數量。這相當於假設不是任何 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)
先前帶有 unique1 < 1000
的範例是對 scalarltsel
實際作用的過度簡化;現在我們已經看到了使用 MCV 的範例,我們可以填寫更多詳細資訊。該範例就其所及是正確的,因為由於 unique1
是一個唯一欄位,因此它沒有 MCV(顯然,沒有值比任何其他值更常見)。對於非唯一欄位,通常會有一個直方圖和一個 MCV 清單,並且直方圖不包括由 MCV 代表的欄位母體部分。我們這樣做是因為它允許更精確的估計。在這種情況下,scalarltsel
直接將條件(例如,「< 1000」)套用到 MCV 清單的每個值,並加總條件為 true 的 MCV 的頻率。這給出了表格 MCV 部分內選擇性的精確估計。然後,以與上述相同的方式使用直方圖來估計表格非 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(同樣,這裡沒有 null 值,否則我們必須將它們排除在外)。我們可以看見值 IAAAAA
幾乎落在第三個直方圖 bucket 的末端。規劃器使用一些相當粗略的關於不同字元頻率的假設,得出估計值 0.298387,表示小於 IAAAAA
的直方圖族群部分。然後我們結合 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)
請注意,從 bitmap index scan 估計返回的行數僅反映與索引一起使用的條件;這很重要,因為它影響後續 heap fetches 的成本估計。
最後,我們將檢查一個涉及 join 的查詢
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)
在 nested-loop join 之前,會評估 tenk1
上的限制條件 unique1 < 50
。 這與之前的範圍範例類似處理。 這次值 50 落在 unique1
直方圖的第一個 bucket 中
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)
join 的限制條件是 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 |
在這種情況下沒有MCVunique2
的資訊,並且所有值似乎都是唯一的(n_distinct = -1),因此我們使用一個依賴於兩個關聯的行數估計值(num_rows,未顯示,但 "tenk")以及欄位 null 比例(兩者皆為零)的演算法
selectivity = (1 - null_frac1) * (1 - null_frac2) / max(num_rows1, num_rows2) = (1 - 0) * (1 - 0) / max(10000, 10000) = 0.0001
這是,從每個關聯的 1 中減去 null 比例,然後除以較大關聯的行數(這個值在非唯一情況下會被縮放)。join 可能發出的行數計算為兩個輸入的笛卡爾積的基數,乘以選擇性
rows = (outer_cardinality * inner_cardinality) * selectivity = (50 * 10000) * 0.0001 = 50
如果這兩個欄位有 MCV 列表,eqjoinsel
將使用 MCV 列表的直接比較來確定欄位族群中由 MCV 代表部分的 join 選擇性。 族群其餘部分的估計遵循此處顯示的相同方法。
請注意,我們將 inner_cardinality
顯示為 10000,也就是 tenk2
的未修改大小。 從 EXPLAIN
輸出中看起來,join 行的估計來自 50 * 1,也就是外部行的數量乘以每次在 tenk2
上進行內部索引掃描獲得的估計行數。 但事實並非如此:join 關係大小是在考慮任何特定 join 計畫之前估計的。 如果一切運作良好,則兩種估計 join 大小的方法將產生大致相同的答案,但由於捨入誤差和其他因素,它們有時會顯著不同。
對於那些對更多細節感興趣的人,表的大小(在任何 WHERE
子句之前)的估計是在 src/backend/optimizer/util/plancat.c
中完成的。 子句選擇性的通用邏輯位於 src/backend/optimizer/path/clausesel.c
中。 特定於運算符的選擇性函數主要位於 src/backend/utils/adt/selfuncs.c
中。
如果您在文件中發現任何不正確、與您使用特定功能的經驗不符或需要進一步澄清之處,請使用此表單回報文件問題。