支援的版本: 目前版本 (17) / 16 / 15 / 14 / 13
開發版本: devel
不支援的版本: 12 / 11

64.1. B-Tree 索引 #

64.1.1. 簡介 #

PostgreSQL 包含標準的btree(多路平衡樹)索引資料結構的實作。任何可以排序成明確線性順序的資料類型都可以透過 btree 索引建立索引。唯一的限制是索引條目不能超過頁面的大約三分之一(如果適用,在 TOAST 壓縮之後)。

由於每個 btree 運算子類別都會對其資料類型強制執行排序順序,因此 btree 運算子類別(或實際上是運算子族)已用作 PostgreSQL 的一般表示法和對排序語意的理解。因此,它們獲得了一些超出僅支援 btree 索引所需的功能,並且系統中與 btreeAM相當遙遠的部分也使用了它們。

64.1.2. B-Tree 運算子類別的行為 #

表 36.3 所示,btree 運算子類別必須提供五個比較運算子,<<==>=>。人們可能會期望 <> 也應該是運算子類別的一部分,但它不是,因為在索引搜尋中使用 <> WHERE 子句幾乎沒有用處。(在某些情況下,規劃器會將 <> 視為與 btree 運算子類別相關聯;但它透過 = 運算子的 negator 連結找到該運算子,而不是從 pg_amop 中找到。)

當多個資料類型共享幾乎相同的排序語意時,它們的運算子類別可以分組到一個運算子族中。這樣做是有利的,因為它允許規劃器對跨類型比較進行推論。族中的每個運算子類別都應包含其輸入資料類型的單一類型運算子(和相關的支援函數),而跨類型比較運算子和支援函數在族中是鬆散的。建議在族中包含一組完整的跨類型運算子,從而確保規劃器可以表示它從傳遞性推導出的任何比較條件。

btree 運算子族必須滿足一些基本假設

  • = 運算子必須是等價關係;也就是說,對於資料類型的所有非空值 ABC

    • A = A 為真(自反律

    • 如果 A = B,則 B = A對稱律

    • 如果 A = BB = C,則 A = C傳遞律

  • < 運算子必須是強排序關係;也就是說,對於所有非空值 ABC

    • A < A 為假(非自反律

    • 如果 A < BB < C,則 A < C傳遞律

  • 此外,排序是完全的;也就是說,對於所有非空值 AB

    • A < BA = BB < A 中恰好有一個為真(三一律

    (三一律證明了比較支援函數的定義。)

其他三個運算子以顯而易見的方式根據 =< 定義,並且必須與它們的行為一致。

對於支援多個資料類型的運算子族,當 ABC 從族中的任何資料類型中提取時,上述定律必須成立。傳遞律是最難確保的,因為在跨類型情況下,它們表示兩個或三個不同運算子的行為是一致的陳述。例如,將 float8numeric 放入同一個運算子族是不行的,至少在目前將 numeric 值轉換為 float8 以與 float8 進行比較的語意下不行。由於 float8 的精度有限,這意味著存在不同的 numeric 值將與相同的 float8 值進行比較,因此傳遞律將失敗。

多重資料型別家族的另一個要求是,在運算子家族中定義的任何隱含或二元強制型別轉換,都不得變更相關的排序。

應該很清楚為什麼 btree 索引需要這些規則在單一資料型別中成立:如果沒有這些規則,就無法排列鍵值。此外,使用不同資料型別的比較鍵的索引搜尋,需要跨兩個資料型別的比較行為合理。btree 索引機制本身並不需要將這些規則擴展到家族中的三個或更多資料型別,但規劃器會為了最佳化的目的而依賴它們。

64.1.3. B-Tree 支援函數 #

表 36.9所示,btree 定義了一個必要的支援函數和四個可選的支援函數。這五個使用者定義的方法是:

order(排序)

對於 btree 運算子家族提供比較運算子的每個資料型別組合,它都必須提供一個比較支援函數,該函數在 pg_amproc 中註冊,支援函數編號為 1,且 amproclefttype/amprocrighttype 等於比較的左側和右側資料型別(也就是說,與匹配的運算子在 pg_amop 中註冊的資料型別相同)。比較函數必須接受兩個非空值 AB,並傳回一個 int32 值,當 A < BA = BA > B 時,該值分別為 < 00> 0。不允許傳回空值:資料型別的所有值都必須可比較。請參閱 src/backend/access/nbtree/nbtcompare.c 中的範例。

如果比較的數值是可定序資料型別,則會使用標準 PG_GET_COLLATION() 機制,將適當的定序 OID 傳遞給比較支援函數。

sortsupport(排序支援)

btree 運算子家族可以選擇性地提供排序支援函數,在支援函數編號 2 下註冊。這些函數允許以比簡單地呼叫比較支援函數更有效的方式,實作用於排序目的的比較。此處涉及的 API 在 src/include/utils/sortsupport.h 中定義。

in_range(範圍內)

btree 運算子家族可以選擇性地提供in_range(範圍內)支援函數,在支援函數編號 3 下註冊。這些函數不會在 btree 索引操作期間使用;相反地,它們擴展了運算子家族的語意,使其可以支援包含 RANGE offset PRECEDINGRANGE offset FOLLOWING 框架邊界型別的視窗子句(請參閱第 4.2.8 節)。基本上,提供的額外資訊是如何以與家族的資料排序相容的方式,新增或減去 offset 值。

in_range 函數必須具有以下簽章:

in_range(val type1, base type1, offset type2, sub bool, less bool)
returns bool

valbase 必須是相同型別,這是運算子家族支援的型別之一(也就是說,它提供排序的型別)。然而,offset 可能是不同的型別,這可能是家族原本不支援的型別。例如,內建的 time_ops 家族提供了一個 in_range 函數,其 offset 的型別為 interval。家族可以為其任何支援的型別和一個或多個 offset 型別提供 in_range 函數。每個 in_range 函數都應在 pg_amproc 中輸入,其中 amproclefttype 等於 type1amprocrighttype 等於 type2

in_range 函數的基本語意取決於兩個布林標記參數。它應該新增或減去 baseoffset,然後將 val 與結果進行比較,如下所示:

  • 如果 !sub!less,則傳回 val >= (base + offset)

  • 如果 !subless,則傳回 val <= (base + offset)

  • 如果 sub!less,則傳回 val >= (base - offset)

  • 如果 subless,則傳回 val <= (base - offset)

在執行此操作之前,函數應檢查 offset 的正負號:如果小於零,則引發錯誤 ERRCODE_INVALID_PRECEDING_OR_FOLLOWING_SIZE (22013),錯誤文字類似於視窗函數中先前或後續的大小無效。(這是 SQL 標準的要求,儘管非標準的運算子家族可能會選擇忽略此限制,因為似乎沒有太大的語意必要性。) 此要求委派給 in_range 函數,以便核心程式碼不必了解對於特定資料型別而言小於零是什麼意思。

另一個期望是,如果 base + offsetbase - offset 會溢位,in_range 函數應盡可能避免引發錯誤。即使該值超出資料型別的範圍,也可以確定正確的比較結果。請注意,如果資料型別包含諸如無限大NaN等概念,可能需要格外小心,以確保 in_range 的結果與運算子家族的正常排序一致。

in_range 函數的結果必須與運算子家族施加的排序一致。準確地說,給定 offsetsub 的任何固定值,則

  • 如果 less = true 的 in_range 對於某些 val1base 為 true,則對於每個 val2 <= val1 與相同的 base,它都必須為 true。

  • 如果 less = true 的 in_range 對於某些 val1base 為 false,則對於每個 val2 >= val1 與相同的 base,它都必須為 false。

  • 如果 less = true 的 in_range 對於某些 valbase1 為 true,則對於每個 base2 >= base1 與相同的 val,它都必須為 true。

  • 如果 in_range 搭配 less = true 對於某些 valbase1 為 false,則對於每個具有相同 valbase2 <= base1,它也必須為 false。

less = false 時,具有相反條件的類似敘述成立。

如果正在排序的類型 (type1) 是可定序的,則會使用標準的 PG_GET_COLLATION() 機制將適當的定序 OID 傳遞給 in_range 函數。

in_range 函數不需要處理 NULL 輸入,並且通常會標記為 strict。

equalimage

選擇性地,btree 運算子族可以提供 equalimage(「相等性表示映像相等性」)支援函數,註冊在支援函數編號 4 之下。這些函數允許核心程式碼判斷何時可以安全地應用 btree 重複資料刪除最佳化。目前,equalimage 函數僅在建立或重建索引時呼叫。

equalimage 函數必須具有以下簽章:

equalimage(opcintype oid) returns bool

傳回值是關於運算子類別和定序的靜態資訊。傳回 true 表示保證運算子類別的 order 函數僅在其 AB 引數也可以互換而不會遺失任何語義資訊時,才會傳回 0(「引數相等」)。未註冊 equalimage 函數或傳回 false 表示無法假設此條件成立。

opcintype 引數是運算子類別索引的資料類型之 pg_type.oid。這是一種便利措施,允許跨運算子類別重複使用相同的底層 equalimage 函數。如果 opcintype 是一種可定序的資料類型,則會使用標準的 PG_GET_COLLATION() 機制將適當的定序 OID 傳遞給 equalimage 函數。

就運算子類別而言,傳回 true 表示重複資料刪除是安全的(或對於將其 OID 傳遞給其 equalimage 函數的定序而言是安全的)。但是,只有當每個索引欄位都使用註冊了 equalimage 函數的運算子類別,並且每個函數在呼叫時實際傳回 true 時,核心程式碼才會認為索引的重複資料刪除是安全的。

映像相等性與簡單的逐位元相等性幾乎相同。有一個細微的差異:當索引 varlena 資料類型時,由於 TOAST 輸入壓縮應用不一致,兩個映像相等資料的磁碟表示可能不是逐位元相等的。TOAST形式上,當運算子類別的 equalimage 函數傳回 true 時,可以安全地假設 datum_image_eq() C 函數將始終與運算子類別的 order 函數一致(前提是將相同的定序 OID 傳遞給 equalimageorder 函數)。

核心程式碼基本上無法根據同一系列中其他運算子類別的詳細資訊來推斷多資料類型系列中運算子類別的「相等性表示映像相等性」狀態。此外,運算子族註冊跨類型 equalimage 函數是不明智的,嘗試這樣做將導致錯誤。這是因為「相等性表示映像相等性」狀態不僅取決於排序/相等語義,這些語義或多或少在運算子族層級定義。一般而言,必須單獨考慮某個特定資料類型實作的語義。

核心 PostgreSQL 發行版本中包含的運算子類別所遵循的慣例是註冊一個普通的、通用的 equalimage 函數。大多數運算子類別註冊 btequalimage(),這表示重複資料刪除是無條件安全的。可定序資料類型(如 text)的運算子類別註冊 btvarstrequalimage(),這表示使用確定性定序進行重複資料刪除是安全的。第三方擴充功能的最佳做法是註冊自己的自訂函數以保留控制權。

options

選擇性地,B 樹運算子族可以提供 options(「運算子類別特定選項」)支援函數,註冊在支援函數編號 5 之下。這些函數定義了一組控制運算子類別行為的使用者可見參數。

options 支援函數必須具有以下簽章:

options(relopts local_relopts *) returns void

該函數會傳遞一個指向 local_relopts 結構的指標,需要使用一組運算子類別特定的選項來填寫該結構。可以使用 PG_HAS_OPCLASS_OPTIONS()PG_GET_OPCLASS_OPTIONS() 巨集從其他支援函數存取這些選項。

目前,沒有 B 樹運算子類別具有 options 支援函數。B 樹不允許像 GiST、SP-GiST、GIN 和 BRIN 那樣靈活地表示鍵。因此,options 可能在目前的 B 樹索引存取方法中沒有太多應用。儘管如此,還是為了統一性將此支援函數新增到 B 樹中,並且可能會在 PostgreSQL 中 B 樹的進一步發展過程中找到用途。

64.1.4. 實作 #

本節涵蓋 B 樹索引實作的詳細資訊,這些資訊可能對進階使用者有用。 有關 B 樹實作的更詳細,以內部為中心的描述,請參閱來源發佈中的 src/backend/access/nbtree/README

64.1.4.1. B 樹結構 #

PostgreSQL B 樹索引是多層樹狀結構,其中樹的每一層都可以用作頁面的雙向連結清單。 單個元頁儲存在索引的第一個區段檔開頭的固定位置。 所有其他頁面都是葉頁或內部頁面。 葉頁是樹的最底層上的頁面。 所有其他層級都由內部頁面組成。 每個葉頁包含指向表格列的元組。 每個內部頁面包含指向樹中下一層的元組。 通常,超過 99% 的所有頁面都是葉頁。 內部頁面和葉頁都使用 第 65.6 節 中描述的標準頁面格式。

當現有的葉頁無法容納傳入的元組時,會將新的葉頁新增到 B 樹索引。 頁面分割操作透過將一部分項目移動到新頁面,為原本屬於溢出頁面的項目騰出空間。 頁面分割還必須在父頁面中插入一個新的指向新頁面的下連結,這可能會導致父頁面依次分割。 頁面分割以遞迴方式「向上串聯」。 當根頁面最終無法容納新的下連結時,會進行根頁面分割操作。 這會透過建立一個比原始根頁面高一層的新根頁面來將一個新層級新增到樹狀結構中。

64.1.4.2. 由下而上的索引刪除 #

B-Tree 索引並不能直接感知在 MVCC 機制下,同一個邏輯資料表的列可能有多個現存版本;對索引而言,每個元組都是一個獨立的物件,需要自己的索引條目。 版本變動(Version churn) 的元組有時會累積,並對查詢延遲和吞吐量產生不利影響。這通常發生在 UPDATE 繁重的工作負載中,在這種情況下,大多數個別的更新無法應用 HOT 最佳化。UPDATE 期間,僅變更由一個索引涵蓋的其中一個欄位的值 總是 需要一組新的索引元組 — 每個資料表上的 每個 索引都需要一個。 特別要注意的是,這包括未被 UPDATE 邏輯修改 的索引。 所有索引都需要指向資料表中最新版本之後繼實體索引元組。 通常,每個索引中的每個新元組都需要與原始 已更新 的元組共存一小段時間(通常在 UPDATE 交易提交後不久)。

B-Tree 索引透過執行由下而上的索引刪除傳遞來增量刪除版本變動的索引元組。 每次刪除傳遞都是針對預期的 版本變動頁面分割 所觸發的反應。這只會發生在未被 UPDATE 陳述式邏輯修改的索引,否則特定頁面中過時版本的集中累積就會發生。 通常可以避免頁面分割,但某些實作層級的啟發式方法可能會無法識別甚至刪除一個垃圾索引元組(在這種情況下,頁面分割或重複資料刪除傳遞會解決傳入的新元組無法放入分葉頁面的問題)。 任何索引掃描必須遍歷的版本(對於任何單一邏輯列)的最糟情況數量是整體系統反應性和吞吐量的重要因素。 由下而上的索引刪除傳遞針對單一分葉頁面中可疑的垃圾元組,基於涉及邏輯列和版本的 定性 區別。 這與 autovacuum 工作程序執行的 由上而下 索引清除形成對比,後者會在超過某些 定量 資料表層級閾值時觸發(請參閱第 24.1.6 節)。

注意

並非所有在 B-Tree 索引中執行的刪除操作都是由下而上的刪除操作。 存在一種類型的索引元組刪除: 簡單索引元組刪除。 這是一個延遲維護操作,會刪除已知可以安全刪除的索引元組(那些項目識別碼的 LP_DEAD 位元已設定的元組)。 與由下而上的索引刪除一樣,簡單索引刪除發生在預期會發生頁面分割時,作為避免分割的一種方法。

簡單刪除是機會主義的,因為它只有在最近的索引掃描在傳遞過程中設定受影響項目的 LP_DEAD 位元時才會發生。 在 PostgreSQL 14 之前,B-Tree 刪除的唯一類型是簡單刪除。 它與由下而上刪除的主要區別在於,只有前者是透過通過索引掃描的活動來機會主義地驅動,而後者專門針對來自 UPDATE 的版本變動,而這些 UPDATE 不會邏輯修改索引欄位。

對於某些工作負載的特定索引,由下而上的索引刪除執行了絕大多數的垃圾索引元組清除工作。 任何 B-Tree 索引都預期會發生這種情況,該索引會受到來自 UPDATE 的顯著版本變動的影響,而這些 UPDATE 很少或永遠不會邏輯修改索引涵蓋的欄位。 純粹透過目標增量刪除傳遞,可以保持每個邏輯列的平均和最糟情況版本數量較低。 某些索引的磁碟大小甚至可能永遠不會增加一個頁面/區塊,即使 UPDATE 導致 持續 版本變動。 即使如此,作為資料表及其每個索引的 集體 清除的一部分,最終也需要透過 VACUUM 操作(通常在 autovacuum 工作程序中執行)進行徹底的 全面清除

VACUUM 不同,由下而上的索引刪除不提供任何關於最舊垃圾索引元組可能有多舊的強烈保證。 不允許任何索引保留在資料表及其所有索引共同擁有的保守截止點之前已死亡的 浮動垃圾 索引元組。 這個基本的資料表層級不變性使其可以安全地回收資料表TIDs。 這就是不同的邏輯列可以重複使用相同的資料表的原因TID一段時間(儘管這永遠不會發生在生命週期跨越相同 VACUUM 週期的兩個邏輯列)。

64.1.4.3. 重複資料刪除 #

重複項是分葉頁面元組(指向資料表列的元組),其中 所有 索引鍵欄位都具有與同一索引中至少一個其他分葉頁面元組的對應欄位值相符的值。 重複元組在實務中非常常見。 當啟用一個可選技術時:重複資料刪除,B-Tree 索引可以使用特殊的、節省空間的重複項表示方式。

重複資料刪除的工作原理是定期將多組重複元組合併在一起,為每個群組形成一個單獨的發佈清單元組。 欄位鍵值只會在這個表示法中出現一次。 接下來是一個排序過的TID指向資料表中的列。 這顯著降低了索引的儲存大小,其中每個值(或每個不同的欄位值組合)平均出現多次。 可以顯著減少查詢的延遲。 整體查詢吞吐量可能會顯著增加。 例行索引 vacuum 的額外負荷也可能會顯著降低。

注意

B-Tree 重複資料刪除與包含 NULL 值的 重複項 一樣有效,即使根據任何 B-Tree 運算子類別的 = 成員,NULL 值永遠不等於彼此。 就任何理解磁碟上 B-Tree 結構的實作部分而言,NULL 只是索引值域中的另一個值。

重複資料刪除過程會在插入一個新的項目且該項目無法放入現有的分葉頁面時以延遲方式發生,但僅當索引元組刪除無法釋放足夠的空間來容納新項目時(通常會簡要考慮刪除,然後跳過)。 與 GIN 發佈清單元組不同,B-Tree 發佈清單元組不需要在每次插入新重複項時都展開;它們只是分葉頁面原始邏輯內容的替代實體表示方式。 這種設計優先考慮混合讀寫工作負載的一致效能。 大多數客戶端應用程式至少會看到使用重複資料刪除帶來適度的效能提升。 預設情況下已啟用重複資料刪除。

CREATE INDEXREINDEX 會應用重複資料刪除來建立張貼清單元組,但它們使用的策略略有不同。從資料表中取得的已排序輸入中遇到的每組重複普通元組,會先合併成一個張貼清單元組加入到目前待處理的分葉頁面中。個別的張貼清單元組會盡可能地塞入許多TID個項目。分葉頁面會以通常的方式寫出,而沒有任何單獨的重複資料刪除程序。這種策略非常適合 CREATE INDEXREINDEX,因為它們是一次性的批次作業。

由於索引中很少或沒有重複的值,因此無法從重複資料刪除中受益的寫入密集型工作負載,將會產生一個小的、固定的效能損失(除非明確停用重複資料刪除)。可以使用 deduplicate_items 儲存參數來停用個別索引內的重複資料刪除。唯讀工作負載永遠不會有任何效能損失,因為讀取張貼清單元組至少與讀取標準元組表示法一樣有效。停用重複資料刪除通常沒有幫助。

有時,唯一索引(以及唯一約束)可以使用重複資料刪除。這允許分葉頁面暫時吸收額外的版本變動重複項。唯一索引中的重複資料刪除增強了由下而上的索引刪除,尤其是在長時間執行的交易持有阻止垃圾收集的快照的情況下。目標是為由下而上的索引刪除策略爭取時間,使其再次生效。延遲頁面分割,直到單個長時間執行的交易自然消失,這樣可以讓由下而上的刪除程序成功,而較早的刪除程序失敗了。

提示

應用了一種特殊的啟發式方法來確定是否應該在唯一索引中進行重複資料刪除程序。它通常可以直接跳到分割分葉頁面,避免因浪費循環時間在無用的重複資料刪除程序上而造成的效能損失。如果您擔心重複資料刪除的開銷,請考慮選擇性地設定 deduplicate_items = off。在唯一索引中啟用重複資料刪除幾乎沒有缺點。

由於實作層級的限制,重複資料刪除並非在所有情況下都能使用。執行 CREATE INDEXREINDEX 時,會判斷重複資料刪除的安全性。

請注意,在以下情況中,如果相等資料之間存在語義上的顯著差異,則會認為重複資料刪除是不安全的,並且無法使用。

  • 當使用非確定性定序時,textvarcharchar 無法使用重複資料刪除。必須保留相等資料之間的大小寫和重音符號差異。

  • numeric 無法使用重複資料刪除。必須保留數值顯示比例相等資料之間。

  • jsonb 無法使用重複資料刪除,因為 jsonb B-Tree 運算子類別在內部使用 numeric

  • float4float8 無法使用重複資料刪除。這些類型對 -00 具有不同的表示形式,但它們仍然被認為是相等的。必須保留這種差異。

在未來版本的 PostgreSQL 中,可能會解除一個進一步的實作層級限制。

  • 容器類型(例如複合類型、陣列或範圍類型)無法使用重複資料刪除。

無論使用的運算子類別或定序為何,都存在一個進一步的實作層級限制。

  • INCLUDE 索引永遠無法使用重複資料刪除。

提交更正

如果您在文件中發現任何不正確、與特定功能的使用體驗不符或需要進一步澄清的地方,請使用此表格來報告文件問題。