2025年9月25日: PostgreSQL 18 釋出!
支援的版本: 當前 (18) / 17 / 16 / 15 / 14 / 13
開發版本: devel
不支援的版本: 12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2 / 8.1

5.12. 表分割槽 #

PostgreSQL 支援基本的表分割槽。本節將介紹為什麼以及如何實施分割槽作為資料庫設計的一部分。

5.12.1. 概述 #

分割槽是指將邏輯上的一張大表分割成更小的物理塊。分割槽可以提供多種好處:

  • 在某些情況下,查詢效能可以得到極大改善,特別是當表中大部分經常訪問的行位於單個分割槽或少數幾個分割槽中時。分割槽有效地替代了索引的上層樹級,使得索引中經常使用的部分更有可能被載入到記憶體中。

  • 當查詢或更新訪問單個分割槽的絕大部分行時,透過對該分割槽進行順序掃描來提高效能,而不是使用索引,因為索引會涉及對整個表分散的隨機讀取。

  • 透過新增或刪除分割槽,可以實現批次載入和刪除,前提是在分割槽設計中考慮了使用模式。刪除單個分割槽使用 DROP TABLE,或者執行 ALTER TABLE DETACH PARTITION,比批次操作要快得多。這些命令還可以完全避免因批次 DELETE 引起的 VACUUM 開銷。

  • 很少使用的資料可以遷移到更便宜、更慢的儲存介質。

這些好處通常只有當表非常大時才值得考慮。表何時會受益於分割槽取決於應用程式,雖然經驗法則是表的大小應超過資料庫伺服器的物理記憶體。

PostgreSQL 提供了以下形式分割槽的內建支援:

範圍分割槽 #

表根據一個或一組鍵列定義的“範圍”進行分割槽,不同分割槽中分配的值範圍之間沒有重疊。例如,可以按日期範圍或特定業務物件的識別符號範圍進行分割槽。每個範圍的邊界被理解為低端包含,高階不包含。例如,如果一個分割槽的範圍是 110,下一個分割槽的範圍是 1020,那麼值 10 屬於第二個分割槽而不是第一個。

列表分割槽 #

表透過明確列出每個分割槽中出現的鍵值來分割槽。

雜湊分割槽 #

表透過指定每個分割槽的模數和餘數進行分割槽。每個分割槽將包含分割槽鍵除以指定模數後餘數相同的行。

如果您的應用程式需要使用上面未列出的其他分割槽形式,可以使用諸如繼承和 UNION ALL 檢視之類的替代方法。這些方法提供了靈活性,但沒有內建宣告式分割槽的一些效能優勢。

5.12.2. 宣告式分割槽 #

PostgreSQL 允許您宣告一個表被劃分為多個分割槽。被分割的表稱為分割槽表。宣告包括分割槽方法(如上所述),以及用於作為分割槽鍵的一組列或表示式。

分割槽表本身是一個“虛擬”表,沒有自己的儲存。相反,儲存屬於分割槽,這些分割槽是與分割槽表關聯的、否則是普通表。每個分割槽根據其分割槽邊界儲存一部分資料。插入到分割槽表的所有行都將根據分割槽鍵列的值路由到相應的一個分割槽。更新行的分割槽鍵將導致該行被移動到不同的分割槽,如果它不再滿足其原始分割槽的分割槽邊界的話。

分割槽本身可以定義為分割槽表,從而形成子分割槽。儘管所有分割槽必須與其父分割槽表具有相同的列,但分割槽可以擁有自己獨立的索引、約束和預設值,與其他分割槽不同。有關建立分割槽表和分割槽的更多詳細資訊,請參閱 CREATE TABLE

不能將普通錶轉換為分割槽表,反之亦然。但是,可以將現有的普通表或分割槽表新增為分割槽表的一個分割槽,或從分割槽表中刪除一個分割槽,使其成為一個獨立的表;這可以簡化和加速許多維護過程。請參閱 ALTER TABLE 以瞭解更多關於 ATTACH PARTITIONDETACH PARTITION 子命令的資訊。

分割槽也可以是外部表,但需要非常小心,因為這時使用者有責任確保外部表的內容滿足分割槽規則。還有一些其他限制。有關更多資訊,請參閱 CREATE FOREIGN TABLE

5.12.2.1. 示例 #

假設我們正在為一家大型冰淇淋公司構建一個數據庫。該公司每天測量最高溫度以及每個地區的冰淇淋銷售量。從概念上講,我們希望有一個類似以下的表:

CREATE TABLE measurement (
    city_id         int not null,
    logdate         date not null,
    peaktemp        int,
    unitsales       int
);

我們知道大多數查詢將只訪問最近一週、一個月或一個季度的資料,因為這個表的主要用途是為管理層準備線上報告。為了減少需要儲存的舊資料量,我們決定只保留最近 3 年的資料。每個月開始時,我們將刪除最舊的一個月的資料。在這種情況下,我們可以使用分割槽來幫助我們滿足測量表的所有不同需求。

要在此案例中使用宣告式分割槽,請執行以下步驟:

  1. 透過指定 PARTITION BY 子句將 measurement 表建立為分割槽表,該子句包括分割槽方法(此處為 RANGE)以及用於分割槽鍵的列列表。

    CREATE TABLE measurement (
        city_id         int not null,
        logdate         date not null,
        peaktemp        int,
        unitsales       int
    ) PARTITION BY RANGE (logdate);
    
  2. 建立分割槽。每個分割槽的定義必須指定與父分割槽方法和分割槽鍵相對應的邊界。請注意,指定邊界會導致新分割槽的取值與一個或多個現有分割槽的取值重疊,這將導致錯誤。

    建立的分割槽在各方面都是標準的 PostgreSQL 表(或可能是外部表)。可以為每個分割槽單獨指定表空間和儲存引數。

    對於我們的示例,每個分割槽應包含一個月的資料,以匹配一次刪除一個月資料的要求。因此,命令可能看起來像:

    CREATE TABLE measurement_y2006m02 PARTITION OF measurement
        FOR VALUES FROM ('2006-02-01') TO ('2006-03-01');
    
    CREATE TABLE measurement_y2006m03 PARTITION OF measurement
        FOR VALUES FROM ('2006-03-01') TO ('2006-04-01');
    
    ...
    CREATE TABLE measurement_y2007m11 PARTITION OF measurement
        FOR VALUES FROM ('2007-11-01') TO ('2007-12-01');
    
    CREATE TABLE measurement_y2007m12 PARTITION OF measurement
        FOR VALUES FROM ('2007-12-01') TO ('2008-01-01')
        TABLESPACE fasttablespace;
    
    CREATE TABLE measurement_y2008m01 PARTITION OF measurement
        FOR VALUES FROM ('2008-01-01') TO ('2008-02-01')
        WITH (parallel_workers = 4)
        TABLESPACE fasttablespace;
    

    (請記住,相鄰分割槽可以共享邊界值,因為範圍的上邊界被視為排他邊界。)

    如果您希望實現子分割槽,請再次在建立單個分割槽的命令中指定 PARTITION BY 子句,例如:

    CREATE TABLE measurement_y2006m02 PARTITION OF measurement
        FOR VALUES FROM ('2006-02-01') TO ('2006-03-01')
        PARTITION BY RANGE (peaktemp);
    

    在建立 measurement_y2006m02 分割槽之後,插入到 measurement 表中對映到 measurement_y2006m02 的任何資料(或者直接插入到 measurement_y2006m02 的資料,如果其分割槽約束得到滿足,這是允許的)將根據 peaktemp 列進一步重定向到一個子分割槽。指定的分割槽鍵可以與父分割槽鍵重疊,儘管在指定子分割槽邊界時應小心,使其接受的資料集構成其自身邊界允許的資料的子集;系統不會嘗試檢查這是否屬實。

    插入到父表中且未對映到現有分割槽的資料將導致錯誤;必須手動新增一個合適的分割槽。

    無需手動建立描述分割槽邊界條件的表約束。這些約束將自動建立。

  3. 在分割槽表上建立鍵列的索引,以及您可能想要的任何其他索引。(鍵索引並非嚴格必需,但在大多數情況下很有用。)這會在每個分割槽上自動建立一個匹配的索引,並且稍後建立或附加的任何分割槽也將具有此類索引。在分割槽表上宣告的索引或唯一約束與分割槽表一樣是“虛擬”的:實際資料位於各個分割槽表上的子索引中。

    CREATE INDEX ON measurement (logdate);
    
  4. 確保 postgresql.conf 中的 enable_partition_pruning 配置引數未被停用。如果停用,查詢將不會按預期進行最佳化。

在上面的示例中,我們每個月都會建立一個新分割槽,因此編寫一個自動生成所需 DDL 的指令碼可能是明智的。

5.12.2.2. 分割槽維護 #

通常,在初始定義表時建立的分割槽集不會保持靜態。通常需要刪除包含舊資料的分割槽,並定期新增新分割槽來處理新資料。分割槽最重要的優點之一正是它允許透過操作分割槽結構來幾乎瞬間地完成這項本應很繁瑣的任務,而不是物理移動大量資料。

刪除舊資料的最簡單選項是刪除不再需要的分割槽:

DROP TABLE measurement_y2006m02;

這可以非常快速地刪除數百萬條記錄,因為它不必單獨刪除每條記錄。但請注意,上述命令要求在父表上獲取 ACCESS EXCLUSIVE 鎖。

另一個通常更優的選擇是將分割槽從分割槽表中移除,但仍將其作為獨立表保留訪問許可權。這有兩種形式:

ALTER TABLE measurement DETACH PARTITION measurement_y2006m02;
ALTER TABLE measurement DETACH PARTITION measurement_y2006m02 CONCURRENTLY;

這允許在刪除資料之前對其執行進一步的操作。例如,這通常是使用 COPYpg_dump 或類似工具備份資料的好時機。也可能是將資料聚合為更小格式、執行其他資料操作或執行報告的好時機。命令的第一種形式要求在父表上獲取 ACCESS EXCLUSIVE 鎖。新增 CONCURRENTLY 限定符(如第二種形式)允許分離操作僅需要父表上的 SHARE UPDATE EXCLUSIVE 鎖,但有關限制的詳細資訊,請參閱 ALTER TABLE ... DETACH PARTITION

同樣,我們可以新增一個新分割槽來處理新資料。我們可以像上面建立原始分割槽一樣,在分割槽表中建立一個空分割槽:

CREATE TABLE measurement_y2008m02 PARTITION OF measurement
    FOR VALUES FROM ('2008-02-01') TO ('2008-03-01')
    TABLESPACE fasttablespace;

作為建立新分割槽的替代方法,有時更方便的是建立一個獨立於分割槽結構的新表,然後將其作為分割槽附加。這允許在新資料出現在分割槽表之前對其進行載入、檢查和轉換。此外,ATTACH PARTITION 操作只需要分割槽表上的 SHARE UPDATE EXCLUSIVE 鎖,而不是 CREATE TABLE ... PARTITION OF 所需的 ACCESS EXCLUSIVE 鎖,因此它對分割槽表的併發操作更友好;有關其他詳細資訊,請參閱 ALTER TABLE ... ATTACH PARTITIONCREATE TABLE ... LIKE 選項可以幫助避免冗餘地重複父表定義;例如:

CREATE TABLE measurement_y2008m02
  (LIKE measurement INCLUDING DEFAULTS INCLUDING CONSTRAINTS)
  TABLESPACE fasttablespace;

ALTER TABLE measurement_y2008m02 ADD CONSTRAINT y2008m02
   CHECK ( logdate >= DATE '2008-02-01' AND logdate < DATE '2008-03-01' );

\copy measurement_y2008m02 from 'measurement_y2008m02'
-- possibly some other data preparation work

ALTER TABLE measurement ATTACH PARTITION measurement_y2008m02
    FOR VALUES FROM ('2008-02-01') TO ('2008-03-01' );

請注意,在執行 ATTACH PARTITION 命令時,將掃描該表以驗證分割槽約束,同時在該分割槽上持有 ACCESS EXCLUSIVE 鎖。如上所示,建議透過在附加表之前在表上建立與預期分割槽約束匹配的 CHECK 約束來避免此掃描。附加完成後,建議刪除現在冗餘的 CHECK 約束。如果正在附加的表本身是一個分割槽表,那麼它的每個子分割槽都會被遞迴鎖定和掃描,直到遇到合適的 CHECK 約束或到達葉分割槽。

同樣,如果分割槽表有一個 DEFAULT 分割槽,建議建立一個 CHECK 約束,排除要附加的分割槽的約束。如果未執行此操作,將掃描 DEFAULT 分割槽以驗證其中不包含應位於要附加的分割槽中的記錄。此操作將在持有 DEFAULT 分割槽上的 ACCESS EXCLUSIVE 鎖時執行。如果 DEFAULT 分割槽本身是一個分割槽表,那麼它的每個分割槽都會像上面提到的要附加的表一樣被遞迴檢查。

如前所述,可以在分割槽表上建立索引,以便它們自動應用於整個層次結構。這可能非常方便,因為不僅所有現有分割槽都會被索引,而且任何未來的分割槽也將被索引。但是,在分割槽表上建立新索引時的一個限制是,無法使用 CONCURRENTLY 限定符,這可能導致長時間的鎖定。要避免此問題,可以使用 CREATE INDEX ON ONLY 分割槽表,這將建立一個標記為無效的新索引,阻止其自動應用於現有分割槽。相反,可以透過使用 CONCURRENTLY 在每個分割槽上單獨建立索引,並使用 ALTER INDEX ... ATTACH PARTITION 將其“附加”到父分割槽上的分割槽索引。一旦所有分割槽的索引都附加到父索引,父索引將自動標記為有效。示例:

CREATE INDEX measurement_usls_idx ON ONLY measurement (unitsales);

CREATE INDEX CONCURRENTLY measurement_usls_200602_idx
    ON measurement_y2006m02 (unitsales);
ALTER INDEX measurement_usls_idx
    ATTACH PARTITION measurement_usls_200602_idx;
...

此技術也可用於 UNIQUEPRIMARY KEY 約束;當建立約束時,索引會隱式建立。示例:

ALTER TABLE ONLY measurement ADD UNIQUE (city_id, logdate);

ALTER TABLE measurement_y2006m02 ADD UNIQUE (city_id, logdate);
ALTER INDEX measurement_city_id_logdate_key
    ATTACH PARTITION measurement_y2006m02_city_id_logdate_key;
...

5.12.2.3. 限制 #

以下限制適用於分割槽表:

  • 要在分割槽表上建立唯一約束或主鍵約束,分割槽鍵不得包含任何表示式或函式呼叫,並且約束的列必須包含所有分割槽鍵列。此限制存在的原因是構成約束的各個索引只能直接在各自分割槽內強制執行唯一性;因此,分割槽結構本身必須保證不同分割槽之間沒有重複項。

  • 類似地,排除約束必須包含所有分割槽鍵列。此外,約束必須對這些列進行相等性比較(而不是例如 &&)。同樣,此限制源於無法強制跨分割槽限制。約束可以包含不屬於分割槽鍵的其他列,並且可以使用您喜歡的任何運算子來比較這些列。

  • 對於 BEFORE ROWINSERT 觸發器,不能更改新行的最終目標分割槽。

  • 不允許在同一個分割槽樹中混合臨時表和永久表。因此,如果分割槽表是永久的,那麼它的分割槽也必須是永久的;如果分割槽表是臨時的,情況也是如此。使用臨時表時,分割槽樹的所有成員都必須來自同一個會話。

分割槽透過底層繼承與它們的分割槽錶鏈接。但是,不能將繼承的所有通用功能用於宣告式分割槽表或其分割槽,如下文所述。特別是,分割槽不能有除其所在分割槽表之外的任何父表,也不能從分割槽表和普通表繼承。這意味著分割槽表及其分割槽從不與普通表共享繼承層次結構。

由於由分割槽表及其分割槽組成的分割槽層次結構仍然是繼承層次結構,因此 tableoid 和繼承的所有正常規則都適用,如 第 5.11 節中所述,但有幾處例外:

  • 分割槽不能包含父表中不存在的列。在使用 CREATE TABLE 建立分割槽時,無法指定列,也不可能事後使用 ALTER TABLE 向分割槽新增列。只有當要附加的表的列與父表完全匹配時,才能使用 ALTER TABLE ... ATTACH PARTITION 將表作為分割槽新增。

  • 分割槽表的 CHECKNOT NULL 約束始終由其所有分割槽繼承;不允許建立這些型別的 NO INHERIT 約束。如果父表中存在相同的約束,則不能刪除這些型別的約束。

  • 使用 ONLY 在分割槽表上單獨新增或刪除約束是支援的,只要沒有分割槽。一旦存在分割槽,對於 UNIQUEPRIMARY KEY 以外的任何約束,使用 ONLY 將導致錯誤。相反,可以在分割槽本身上新增(如果它們不在父表中)和刪除約束。

  • 由於分割槽表本身沒有資料,嘗試在分割槽表上使用 TRUNCATE ONLY 將始終返回錯誤。

5.12.3. 使用繼承進行分割槽 #

雖然內建的宣告式分割槽適用於大多數常見用例,但在某些情況下,更靈活的方法可能很有用。分割槽可以使用表繼承來實現,這允許實現宣告式分割槽不支援的一些功能,例如:

  • 對於宣告式分割槽,分割槽必須與分割槽表具有完全相同的列集,而在表繼承中,子表可以具有父表中不存在的額外列。

  • 表繼承允許多重繼承。

  • 宣告式分割槽僅支援範圍、列表和雜湊分割槽,而表繼承允許使用者按選擇的方式劃分資料。(但請注意,如果約束排除無法有效剪枝子表,查詢效能可能會很差。)

5.12.3.1. 示例 #

本示例構建一個與上述宣告式分割槽示例等效的分割槽結構。使用以下步驟:

  1. 建立“根”表,所有“子”表都將從此繼承。此表將不包含任何資料。不要在此表上定義任何檢查約束,除非您希望它們平等地應用於所有子表。也沒有必要在此表上定義任何索引或唯一約束。對於我們的示例,根表是最初定義的 measurement 表:

    CREATE TABLE measurement (
        city_id         int not null,
        logdate         date not null,
        peaktemp        int,
        unitsales       int
    );
    
  2. 建立幾個“子”表,每個表都從根表繼承。通常,這些表不會向從根表繼承的列集中新增任何列。與宣告式分割槽一樣,這些表在各方面都是標準的 PostgreSQL 表(或外部表)。

    CREATE TABLE measurement_y2006m02 () INHERITS (measurement);
    CREATE TABLE measurement_y2006m03 () INHERITS (measurement);
    ...
    CREATE TABLE measurement_y2007m11 () INHERITS (measurement);
    CREATE TABLE measurement_y2007m12 () INHERITS (measurement);
    CREATE TABLE measurement_y2008m01 () INHERITS (measurement);
    
  3. 在子表上新增非重疊的表約束,以定義每個表中允許的鍵值。

    典型的例子是:

    CHECK ( x = 1 )
    CHECK ( county IN ( 'Oxfordshire', 'Buckinghamshire', 'Warwickshire' ))
    CHECK ( outletID >= 100 AND outletID < 200 )
    

    確保約束保證不同子表中允許的鍵值之間沒有重疊。常見的錯誤是設定範圍約束,例如:

    CHECK ( outletID BETWEEN 100 AND 200 )
    CHECK ( outletID BETWEEN 200 AND 300 )
    

    這是錯誤的,因為不清楚鍵值 200 應該屬於哪個子表。相反,範圍應定義為:

    CREATE TABLE measurement_y2006m02 (
        CHECK ( logdate >= DATE '2006-02-01' AND logdate < DATE '2006-03-01' )
    ) INHERITS (measurement);
    
    CREATE TABLE measurement_y2006m03 (
        CHECK ( logdate >= DATE '2006-03-01' AND logdate < DATE '2006-04-01' )
    ) INHERITS (measurement);
    
    ...
    CREATE TABLE measurement_y2007m11 (
        CHECK ( logdate >= DATE '2007-11-01' AND logdate < DATE '2007-12-01' )
    ) INHERITS (measurement);
    
    CREATE TABLE measurement_y2007m12 (
        CHECK ( logdate >= DATE '2007-12-01' AND logdate < DATE '2008-01-01' )
    ) INHERITS (measurement);
    
    CREATE TABLE measurement_y2008m01 (
        CHECK ( logdate >= DATE '2008-01-01' AND logdate < DATE '2008-02-01' )
    ) INHERITS (measurement);
    
  4. 對於每個子表,在鍵列上建立索引,以及您可能想要的任何其他索引。

    CREATE INDEX measurement_y2006m02_logdate ON measurement_y2006m02 (logdate);
    CREATE INDEX measurement_y2006m03_logdate ON measurement_y2006m03 (logdate);
    CREATE INDEX measurement_y2007m11_logdate ON measurement_y2007m11 (logdate);
    CREATE INDEX measurement_y2007m12_logdate ON measurement_y2007m12 (logdate);
    CREATE INDEX measurement_y2008m01_logdate ON measurement_y2008m01 (logdate);
    
  5. 我們希望我們的應用程式能夠說 INSERT INTO measurement ...,並且資料能夠被重定向到合適的子表。我們可以透過將合適的觸發器函式附加到根表來實現。如果資料只新增到最新的子表中,我們可以使用非常簡單的觸發器函式:

    CREATE OR REPLACE FUNCTION measurement_insert_trigger()
    RETURNS TRIGGER AS $$
    BEGIN
        INSERT INTO measurement_y2008m01 VALUES (NEW.*);
        RETURN NULL;
    END;
    $$
    LANGUAGE plpgsql;
    

    建立函式後,我們建立一個呼叫觸發器函式的觸發器:

    CREATE TRIGGER insert_measurement_trigger
        BEFORE INSERT ON measurement
        FOR EACH ROW EXECUTE FUNCTION measurement_insert_trigger();
    

    我們必須每月重新定義觸發器函式,以便它始終插入到當前的子表中。但不需要更新觸發器定義。

    我們可能希望插入資料,並讓伺服器自動找到應將行新增到的子表。我們可以透過一個更復雜的觸發器函式來實現,例如:

    CREATE OR REPLACE FUNCTION measurement_insert_trigger()
    RETURNS TRIGGER AS $$
    BEGIN
        IF ( NEW.logdate >= DATE '2006-02-01' AND
             NEW.logdate < DATE '2006-03-01' ) THEN
            INSERT INTO measurement_y2006m02 VALUES (NEW.*);
        ELSIF ( NEW.logdate >= DATE '2006-03-01' AND
                NEW.logdate < DATE '2006-04-01' ) THEN
            INSERT INTO measurement_y2006m03 VALUES (NEW.*);
        ...
        ELSIF ( NEW.logdate >= DATE '2008-01-01' AND
                NEW.logdate < DATE '2008-02-01' ) THEN
            INSERT INTO measurement_y2008m01 VALUES (NEW.*);
        ELSE
            RAISE EXCEPTION 'Date out of range.  Fix the measurement_insert_trigger() function!';
        END IF;
        RETURN NULL;
    END;
    $$
    LANGUAGE plpgsql;
    

    觸發器定義與之前相同。請注意,每個 IF 測試必須與子表的 CHECK 約束精確匹配。

    雖然這個函式比單月情況更復雜,但它不需要經常更新,因為可以提前新增分支以備需要。

    注意

    實際上,如果大多數插入都進入最新的子表,最好先檢查最新的子表。為了簡單起見,我們在示例的其他部分中以相同的順序顯示了觸發器的測試。

    將插入重定向到合適子表的另一種方法是在根表上設定規則,而不是觸發器。例如:

    CREATE RULE measurement_insert_y2006m02 AS
    ON INSERT TO measurement WHERE
        ( logdate >= DATE '2006-02-01' AND logdate < DATE '2006-03-01' )
    DO INSTEAD
        INSERT INTO measurement_y2006m02 VALUES (NEW.*);
    ...
    CREATE RULE measurement_insert_y2008m01 AS
    ON INSERT TO measurement WHERE
        ( logdate >= DATE '2008-01-01' AND logdate < DATE '2008-02-01' )
    DO INSTEAD
        INSERT INTO measurement_y2008m01 VALUES (NEW.*);
    

    規則比觸發器具有顯著的額外開銷,但開銷是每個查詢支付一次,而不是每行支付一次,因此此方法可能在批次插入情況下有利。但在大多數情況下,觸發器方法將提供更好的效能。

    請注意,COPY 會忽略規則。如果您想使用 COPY 插入資料,您需要將資料複製到正確的子表中,而不是直接複製到根表中。COPY 會觸發觸發器,因此如果您使用觸發器方法,您可以正常使用它。

    規則方法的另一個缺點是,沒有簡單的方法可以在規則集未涵蓋插入日期時強制執行錯誤;資料將默默地進入根表。

  6. 確保 postgresql.conf 中的 constraint_exclusion 配置引數未被停用;否則可能會不必要地訪問子表。

正如我們所見,複雜表層次結構可能需要大量的 DDL。在上面的示例中,我們每個月都會建立一個新的子表,因此編寫一個自動生成所需 DDL 的指令碼可能是明智的。

5.12.3.2. 繼承分割槽的維護 #

要快速刪除舊資料,只需刪除不再需要子表:

DROP TABLE measurement_y2006m02;

要將子表從繼承層次結構表中移除,但仍將其作為獨立表保留訪問許可權:

ALTER TABLE measurement_y2006m02 NO INHERIT measurement;

要新增一個新子表來處理新資料,只需像上面建立原始子表一樣建立一個空的子表:

CREATE TABLE measurement_y2008m02 (
    CHECK ( logdate >= DATE '2008-02-01' AND logdate < DATE '2008-03-01' )
) INHERITS (measurement);

或者,您可能希望在將新子表新增到表層次結構之前建立並填充它。這可以允許在資料對父表的查詢可見之前對其進行載入、檢查和轉換。

CREATE TABLE measurement_y2008m02
  (LIKE measurement INCLUDING DEFAULTS INCLUDING CONSTRAINTS);
ALTER TABLE measurement_y2008m02 ADD CONSTRAINT y2008m02
   CHECK ( logdate >= DATE '2008-02-01' AND logdate < DATE '2008-03-01' );
\copy measurement_y2008m02 from 'measurement_y2008m02'
-- possibly some other data preparation work
ALTER TABLE measurement_y2008m02 INHERIT measurement;

5.12.3.3. 注意事項 #

以下注意事項適用於透過繼承實現的分割槽:

  • 沒有自動方法來驗證所有 CHECK 約束是否相互排斥。建立生成子表並建立/修改相關物件的程式碼,而不是手動編寫每個物件,會更安全。

  • 索引和外部索引鍵約束應用於單個表,而不是它們的繼承子表,因此它們有一些需要注意的 注意事項

  • 此處顯示的方案假定行鍵列的值永不更改,或者至少不會更改到需要將其移動到另一個分割槽的程度。嘗試進行此類更改的 UPDATE 會因 CHECK 約束而失敗。如果您需要處理這種情況,可以在子表上放置適當的更新觸發器,但這會使結構管理複雜得多。

  • 手動 VACUUMANALYZE 命令會自動處理所有繼承子表。如果這是不希望的,可以使用 ONLY 關鍵字。例如,命令:

    ANALYZE ONLY measurement;
    

    將只處理根表。

  • 帶有 ON CONFLICT 子句的 INSERT 語句不太可能按預期工作,因為 ON CONFLICT 操作僅在指定目標關係(而非其子關係)的唯一性衝突時執行。

  • 除非應用程式明確瞭解分割槽方案,否則需要觸發器或規則來將行路由到所需的子表。編寫觸發器可能很複雜,並且比宣告式分割槽內部執行的元組路由慢得多。

5.12.4. 分割槽剪枝 #

分割槽剪枝是一種查詢最佳化技術,可提高宣告式分割槽表的效能。例如:

SET enable_partition_pruning = on;                 -- the default
SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01';

沒有分割槽剪枝,上述查詢將掃描 measurement 表的每個分割槽。啟用分割槽剪枝後,規劃器將檢查每個分割槽的定義,並證明該分割槽不需要掃描,因為它可能不包含滿足查詢 WHERE 子句的任何行。當規劃器能夠證明這一點時,它會從查詢計劃中排除(剪枝)該分割槽。

透過使用 EXPLAIN 命令和 enable_partition_pruning 配置引數,可以顯示已剪枝分割槽和未剪枝分割槽的計劃之間的區別。此表設定的典型未最佳化計劃是:

SET enable_partition_pruning = off;
EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01';
                                    QUERY PLAN
-------------------------------------------------------------------​----------------
 Aggregate  (cost=188.76..188.77 rows=1 width=8)
   ->  Append  (cost=0.00..181.05 rows=3085 width=0)
         ->  Seq Scan on measurement_y2006m02  (cost=0.00..33.12 rows=617 width=0)
               Filter: (logdate >= '2008-01-01'::date)
         ->  Seq Scan on measurement_y2006m03  (cost=0.00..33.12 rows=617 width=0)
               Filter: (logdate >= '2008-01-01'::date)
...
         ->  Seq Scan on measurement_y2007m11  (cost=0.00..33.12 rows=617 width=0)
               Filter: (logdate >= '2008-01-01'::date)
         ->  Seq Scan on measurement_y2007m12  (cost=0.00..33.12 rows=617 width=0)
               Filter: (logdate >= '2008-01-01'::date)
         ->  Seq Scan on measurement_y2008m01  (cost=0.00..33.12 rows=617 width=0)
               Filter: (logdate >= '2008-01-01'::date)

部分或全部分割槽可能使用索引掃描而不是全表順序掃描,但關鍵在於無需掃描舊分割槽即可回答此查詢。當我們啟用分割槽剪枝時,我們會獲得一個明顯更便宜的計劃,該計劃將返回相同的結果:

SET enable_partition_pruning = on;
EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01';
                                    QUERY PLAN
-------------------------------------------------------------------​----------------
 Aggregate  (cost=37.75..37.76 rows=1 width=8)
   ->  Seq Scan on measurement_y2008m01  (cost=0.00..33.12 rows=617 width=0)
         Filter: (logdate >= '2008-01-01'::date)

請注意,分割槽剪枝僅由分割槽鍵隱式定義的約束驅動,而不是由索引的存在驅動。因此,不必為鍵列定義索引。對於給定的分割槽是否需要建立索引取決於您是否預計掃描該分割槽的查詢將通常掃描分割槽的大部分還是隻是一小部分。在後一種情況下,索引會有幫助,但在前一種情況下則不會。

分割槽剪枝不僅可以在給定查詢的規劃期間執行,還可以在其執行期間執行。這很有用,因為它可以在子句包含在查詢規劃時值未知​​的表示式時剪枝更多分割槽,例如,在 PREPARE 語句中定義的引數,使用子查詢獲得的值,或者在巢狀迴圈連線的內側使用引數化值。分割槽剪枝在執行期間可以在以下任何時間執行:

  • 在查詢計劃初始化期間。分割槽剪枝可以在此為在執行初始化階段已知的引數值執行。在此階段剪枝的分割槽將不會顯示在查詢的 EXPLAINEXPLAIN ANALYZE 中。透過觀察 EXPLAIN 輸出中的“已移除的子計劃”屬性,可以確定在此階段移除的分割槽數量。查詢規劃器為計劃中的所有分割槽獲取鎖。但是,當執行器使用快取的計劃時,鎖僅在經過執行初始化階段分割槽剪枝後剩餘的分割槽上獲取,即在 EXPLAIN 輸出中顯示的分割槽,而不是“已移除的子計劃”屬性引用的那些分割槽。

  • 在查詢計劃實際執行期間。分割槽剪枝也可以在此處執行,以使用僅在實際查詢執行期間才知道的值來移除分割槽。這包括來自子查詢的值以及來自執行時引數的值,例如來自引數化巢狀迴圈連線的值。由於這些引數的值在查詢執行期間可能多次更改,因此每當使用分割槽剪枝的執行引數之一發生更改時,都會執行分割槽剪枝。確定在此階段是否進行了分割槽剪枝需要仔細檢查 EXPLAIN ANALYZE 輸出中的 loops 屬性。對應於不同分割槽的子計劃可能具有不同的值,具體取決於它們在執行期間被剪枝的次數。有些可能顯示為 (never executed),如果它們每次都被剪枝的話。

可以透過 enable_partition_pruning 設定停用分割槽剪枝。

5.12.5. 分割槽與約束排除 #

約束排除是一種與分割槽剪枝類似的查詢最佳化技術。雖然它主要用於使用傳統繼承方法實現的分割槽,但它也可以用於其他目的,包括宣告式分割槽。

約束排除的工作方式與分割槽剪枝非常相似,不同之處在於它使用每個表的 CHECK 約束——這就是它得名原因——而分割槽剪枝使用表的“分割槽邊界”,後者僅在宣告式分割槽的情況下存在。另一個不同之處在於,約束排除僅在規劃時應用;在執行時不會嘗試移除分割槽。

約束排除使用 CHECK 約束這一事實,使其與分割槽剪枝相比速度較慢,但有時也可以作為優勢:因為約束可以定義在宣告式分割槽表上(除了其內部分割槽邊界之外),約束排除可能能夠從查詢計劃中排除更多分割槽。

預設(也是推薦)的 constraint_exclusion 設定既不是 on 也不是 off,而是名為 partition 的中間設定,它會導致該技術僅應用於可能在處理繼承分割槽表的查詢。 on 設定會導致規劃器檢查所有查詢的 CHECK 約束,即使是簡單的、不太可能受益的查詢。

以下注意事項適用於約束排除:

  • 與分割槽剪枝(可以在查詢執行期間應用)不同,約束排除僅在查詢規劃期間應用。

  • 約束排除僅在查詢的 WHERE 子句包含常量(或外部提供的引數)時有效。例如,與非易變函式(如 CURRENT_TIMESTAMP)的比較無法最佳化,因為規劃器無法知道該函式的值在執行時可能落入哪個子表。

  • 保持分割槽約束簡單,否則規劃器可能無法證明子表可能不需要被訪問。對於列表分割槽,使用簡單的相等性條件;對於範圍分割槽,使用簡單的範圍測試,如前面示例所示。一個好的經驗法則是,分割槽約束應僅包含使用 B-tree 可索引運算子將分割槽列與常量進行比較,因為只有 B-tree 可索引的列允許在分割槽鍵中使用。

  • 在約束排除期間會檢查父表的所有子表上的所有約束,因此大量的子表可能會顯著增加查詢規劃時間。因此,基於傳統繼承的分割槽最多可以與一百個子表一起良好工作;不要嘗試使用數千個子表。

5.12.6. 宣告式分割槽的最佳實踐 #

如何分割槽表的選擇應謹慎做出,因為糟糕的設計可能會對查詢規劃和執行的效能產生負面影響。

最關鍵的設計決策之一將是用於分割槽資料的列或列集。通常,最佳選擇是按最常出現在已分割槽表上執行的查詢的 WHERE 子句中的列或列集進行分割槽。與分割槽邊界約束相容的 WHERE 子句可用於剪枝不需要的分割槽。但是,您可能需要根據 PRIMARY KEYUNIQUE 約束的要求做出其他決定。移除不需要的資料也是在規劃分割槽策略時需要考慮的因素。可以相當快地分離整個分割槽,因此將分割槽策略設計成一次要移除的所有資料都位於單個分割槽中可能是有益的。

選擇表應劃分的目標分割槽數也是一個關鍵的決策。分割槽數不足可能意味著索引仍然過大,資料區域性性仍然很差,這可能導致快取命中率低。然而,將表劃分為過多的分割槽也會導致問題。過多的分割槽可能導致查詢規劃時間變長,並且在查詢規劃和執行期間消耗的記憶體會更高,如下文所述。在選擇如何分割槽表時,考慮未來可能發生的變化也很重要。例如,如果您選擇每個客戶一個分割槽,而您目前只有少數幾個大客戶,請考慮幾年後您發現自己有大量小客戶的情況。在這種情況下,最好選擇按 HASH 分割槽並選擇一個合理的分割槽數,而不是嘗試按 LIST 分割槽,並希望客戶數量不會超出按分區劃分資料的實際可行範圍。

子分割槽可以用於進一步劃分預計會比其他分割槽大的分割槽。另一種選擇是使用具有多個分割槽鍵列的範圍分割槽。這兩種方法都可能輕易導致分割槽過多,因此建議謹慎。

重要的是要考慮分割槽在查詢規劃和執行期間的開銷。查詢規劃器通常可以很好地處理具有數千個分割槽的分割槽層次結構,前提是典型查詢允許查詢規劃器將分割槽剪枝到只剩少量分割槽。當規劃器執行分割槽剪枝後剩餘的分割槽數量較多時,規劃時間會變長,記憶體消耗會變高。擁有大量分割槽的另一個原因是伺服器的記憶體消耗可能會隨著時間的推移而顯著增長,特別是如果許多會話訪問大量分割槽。這是因為每個分割槽都需要將其元資料載入到訪問它的每個會話的本地記憶體中。

對於資料倉庫型別的負載,使用比 OLTP 型別負載更多的分割槽是合理的。OLTP型別的工作負載。通常,在資料倉庫中,查詢規劃時間不是問題,因為大部分處理時間都花費在查詢執行期間。對於這兩種型別的工作負載,儘早做出正確的決定很重要,因為重新分割槽大量資料可能會非常緩慢。模擬預期的工作負載通常有助於最佳化分割槽策略。永遠不要僅僅假設更多的分割槽比更少的分割槽好,反之亦然。

提交更正

如果您在文件中看到任何不正確的內容、與您對特定功能的體驗不符的內容或需要進一步說明的內容,請使用 此表格 報告文件問題。