PostgreSQL 支援基本的表格分割 (table partitioning)。本節描述為什麼以及如何將分割作為資料庫設計的一部分來實作。
分割 (Partitioning) 指的是將邏輯上的一個大型表格分割成較小的物理片段。分割可以提供幾個好處
在某些情況下,查詢效能可以大幅提升,特別是當表格中大多數經常存取的列 (row) 位於單個分割區或少數分割區中時。分割有效地替代了索引的上層樹狀結構,使得索引中經常使用的部分更有可能放入記憶體中。
當查詢或更新存取單個分割區的很大一部分時,可以使用該分割區的循序掃描來提升效能,而不是使用索引,這將需要分散在整個表格中的隨機存取讀取。
如果分割設計考慮了使用模式,則可以透過新增或移除分割區來完成大量載入和刪除。使用 DROP TABLE
刪除個別分割區,或執行 ALTER TABLE DETACH PARTITION
,比大量操作快得多。這些指令也可以完全避免大量 DELETE
造成的 VACUUM
額外負荷。
不常使用的資料可以遷移到更便宜且速度較慢的儲存媒體。
只有當表格非常大時,這些好處通常才值得。表格從分割中受益的確切時間點取決於應用程式,儘管一個經驗法則是表格的大小應超過資料庫伺服器的實體記憶體。
PostgreSQL 提供對以下形式分割的內建支援
表格被分割成由鍵值欄位或一組欄位定義的 “範圍 (ranges)”,分配給不同分割區的值範圍之間沒有重疊。例如,可以按日期範圍或特定商業物件的識別碼範圍進行分割。每個範圍的邊界都被理解為在下限處包含 (inclusive),在上限處排除 (exclusive)。例如,如果一個分割區的範圍是從 1
到 10
,而下一個分割區的範圍是從 10
到 20
,那麼值 10
屬於第二個分割區,而不是第一個分割區。
表格被分割成明確列出每個分割區中出現的鍵值。
透過為每個分割區指定模數 (modulus) 和餘數 (remainder) 來分割表格。每個分割區將保存分割鍵的雜湊值除以指定的模數將產生指定餘數的列。
如果您的應用程式需要使用上面未列出的其他分割形式,則可以改為使用繼承和 UNION ALL
檢視表等替代方法。這些方法提供了靈活性,但沒有內建宣告式分割的一些效能優勢。
PostgreSQL 允許您宣告表格被劃分為分割區。被劃分的表格稱為分割表格 (partitioned table)。宣告包括如上所述的分割方法 (partitioning method),加上要用作分割鍵 (partition key) 的欄位或表達式清單。
分割表格本身是一個 “虛擬 (virtual)” 表格,沒有自己的儲存空間。相反,儲存空間屬於分割區 (partitions),分割區是與分割表格關聯的普通表格。每個分割區儲存資料的子集,如其分割邊界 (partition bounds) 所定義。根據分割鍵欄位的數值,插入分割表格的所有列將被路由到相應的分割區。如果列不再滿足其原始分割區的分割邊界,則更新列的分割鍵會導致其移動到不同的分割區。
分割區本身可以定義為已分割的表格,產生子分割區。雖然所有分割區必須與其父分割區具有相同的欄位,但分割區可以有自己的索引、約束和預設值,與其他分割區不同。有關建立分割表格和分割區的更多詳細資訊,請參閱CREATE TABLE。
無法將一般表格轉換為分割表格,反之亦然。但是,可以將現有的普通表格或分割表格新增為分割表格的分割區,或者從分割表格中移除分割區,使其成為獨立表格;這可以簡化並加速許多維護過程。請參閱ALTER TABLE,以了解有關 ATTACH PARTITION
和 DETACH PARTITION
子命令的更多資訊。
分割區也可以是外部表格,但需要非常小心,因為使用者有責任確保外部表格的內容符合分割規則。還有一些其他的限制。有關更多資訊,請參閱CREATE FOREIGN TABLE。
假設我們正在為一家大型冰淇淋公司構建資料庫。該公司每天測量最高溫度,以及每個地區的冰淇淋銷售額。從概念上講,我們想要一個像這樣的表格
CREATE TABLE measurement ( city_id int not null, logdate date not null, peaktemp int, unitsales int );
我們知道大多數查詢只會存取最近一週、一個月或一個季度的資料,因為此表格的主要用途是為管理階層準備線上報告。為了減少需要儲存的舊資料量,我們決定只保留最近 3 年的資料。在每個月的月初,我們將移除最舊月份的資料。在這種情況下,我們可以使用分割來幫助我們滿足對 measurements 表格的所有不同需求。
要在這種情況下使用宣告式分割,請使用以下步驟
通過指定 PARTITION BY
子句建立 measurement
表格作為分割表格,其中包括分割方法(在本例中為 RANGE
)和要用作分割索引鍵的欄位清單。
CREATE TABLE measurement ( city_id int not null, logdate date not null, peaktemp int, unitsales int ) PARTITION BY RANGE (logdate);
建立分割區。每個分割區的定義必須指定與父分割區的分割方法和分割索引鍵相對應的邊界。請注意,指定邊界導致新分割區的值與一個或多個現有分割區中的值重疊將導致錯誤。
如此建立的分割區在各方面都是正常的 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
欄位進一步重新導向到其其中一個分割區。指定的分割索引鍵可能與父分割區的分割索引鍵重疊,但在指定子分割區的邊界時應小心,以確保它接受的資料集構成分割區自身邊界允許的子集;系統不會嘗試檢查是否確實如此。
將資料插入到父表格中,如果沒有對應到現有的分割區之一,將會導致錯誤;必須手動新增適當的分割區。
沒有必要手動建立表格約束來描述分割區的分割邊界條件。這些約束將自動建立。
在分割表格上建立索引鍵欄位和任何其他您可能需要的索引。(索引鍵索引不是絕對必要的,但在大多數情況下它很有幫助。)這會自動在每個分割區上建立匹配的索引,並且您稍後建立或附加的任何分割區也會有這樣的索引。在分割表格上宣告的索引或唯一約束與分割表格一樣是「虛擬」:實際資料位於個別分割表格上的子索引中。
CREATE INDEX ON measurement (logdate);
確保在 postgresql.conf
中沒有停用 enable_partition_pruning 組態參數。如果停用了,查詢將不會按照期望的方式進行最佳化。
在上面的範例中,我們將每個月建立一個新的分割區,因此編寫一個自動產生所需 DDL 的腳本可能是明智之舉。
通常,最初定義表格時建立的分割區集合並不意味著保持靜態。通常需要移除包含舊資料的分割區,並定期為新資料新增新的分割區。分割最重要的優點之一正是它允許通過操作分割結構來幾乎立即執行此項原本痛苦的任務,而不是在物理上移動大量資料。
移除舊資料最簡單的選項是刪除不再需要的分割區
DROP TABLE measurement_y2006m02;
這可以非常快速地刪除數百萬條記錄,因為它不必單獨刪除每條記錄。但請注意,上面的指令需要在父表格上取得 ACCESS EXCLUSIVE
鎖定。
另一個通常更可取的選項是從分割表格中移除分割區,但保留對其作為獨立表格的存取權。這有兩種形式
ALTER TABLE measurement DETACH PARTITION measurement_y2006m02; ALTER TABLE measurement DETACH PARTITION measurement_y2006m02 CONCURRENTLY;
這些允許在丟棄資料之前對資料執行更多操作。例如,這通常是使用 COPY
、pg_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 PARTITION
。 CREATE 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
約束,以避免掃描。一旦 ATTACH PARTITION
完成,建議刪除現在多餘的 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; ...
此技術也可以用於 UNIQUE
和 PRIMARY 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; ...
以下限制適用於已分割的表格
若要在已分割的表格上建立唯一或主鍵約束,分割區鍵不得包含任何運算式或函數呼叫,且約束的欄位必須包含所有分割區鍵欄位。存在此限制的原因是,組成約束的個別索引只能直接在其自己的分割區內強制執行唯一性;因此,分割區結構本身必須保證不同的分割區中沒有重複項。
同樣地,排除約束必須包含所有分割區鍵欄位。此外,約束必須比較這些欄位是否相等(例如,不是 &&
)。同樣地,此限制源於無法強制執行跨分割區的限制。約束可能包含不屬於分割區鍵的其他欄位,並且可以使用您喜歡的任何運算符比較這些欄位。
BEFORE ROW
觸發程序在 INSERT
上無法變更新列的最終目的地分割區。
不允許在同一個分割區樹中混合臨時和永久關係。因此,如果已分割的表格是永久的,那麼它的分割區也必須是永久的,同樣地,如果已分割的表格是臨時的,它的分割區也必須是臨時的。當使用臨時關係時,分割區樹的所有成員必須來自同一個會話。
個別分割區使用底層繼承連結到它們的已分割表格。然而,無法將所有泛型繼承功能與宣告式分割的表格或其分割區一起使用,如下所述。特別是,分割區不能有任何父項,除非它是其分割的已分割表格,表格也不能同時繼承自已分割的表格和常規表格。這意味著已分割的表格及其分割區永遠不會與常規表格共享繼承階層。
由於由已分割的表格及其分割區組成的分割區階層仍然是一個繼承階層,因此 tableoid
和所有正常的繼承規則都適用,如 第 5.11 節所述,但有幾個例外情況
分割區不能具有父表格中不存在的欄位。使用 CREATE TABLE
建立分割區時,無法指定欄位,並且在使用 ALTER TABLE
之後也無法將欄位新增到分割區。只有當表格的欄位與父表格完全匹配時,才能使用 ALTER TABLE ... ATTACH PARTITION
將表格新增為分割區。
已分割表格的 CHECK
和 NOT NULL
約束始終由其所有分割區繼承。標記為 NO INHERIT
的 CHECK
約束不允許在已分割的表格上建立。如果父表格中存在相同的約束,則無法刪除分割區欄位上的 NOT NULL
約束。
只要沒有分割區,就支援使用 ONLY
在僅限已分割的表格上新增或刪除約束。一旦存在分割區,對於 UNIQUE
和 PRIMARY KEY
以外的任何約束,使用 ONLY
都會導致錯誤。相反地,可以在分割區本身上新增約束,並且(如果它們不存在於父表格中)可以刪除。
由於已分割的表格本身沒有任何資料,因此嘗試在已分割的表格上使用 TRUNCATE
ONLY
將始終返回錯誤。
雖然內建的宣告式分割適用於大多數常見的使用案例,但在某些情況下,更靈活的方法可能更有用。 可以使用資料表繼承來實現分割,它允許一些宣告式分割不支援的功能,例如:
對於宣告式分割,分割區必須具有與分割資料表完全相同的欄位集合;而使用資料表繼承,子資料表可以有父資料表沒有的額外欄位。
資料表繼承允許多次繼承。
宣告式分割僅支援範圍、列表和雜湊分割,而資料表繼承允許以使用者選擇的方式分割資料。(但是請注意,如果約束排除無法有效地修剪子資料表,則查詢效能可能會很差。)
此範例建構一個等同於上述宣告式分割範例的分割結構。 使用以下步驟:
建立 「根」 資料表,所有 「子」 資料表都將從其繼承。 此資料表將不包含任何資料。 除非您希望它們平等地應用於所有子資料表,否則請勿在此資料表上定義任何檢查約束。 在其上定義任何索引或唯一約束也沒有意義。 對於我們的範例,根資料表是最初定義的 measurement
資料表。
CREATE TABLE measurement ( city_id int not null, logdate date not null, peaktemp int, unitsales int );
建立多個 「子」 資料表,每個資料表都從根資料表繼承。 通常,這些資料表不會將任何欄位新增到從根繼承的集合中。 與宣告式分割一樣,這些資料表在各方面都是普通的 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);
將非重疊的資料表約束新增到子資料表,以定義每個資料表中允許的鍵值。
典型的範例是:
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);
對於每個子資料表,在鍵欄位上建立索引,以及您可能需要的任何其他索引。
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);
我們希望我們的應用程式能夠說 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
會觸發觸發器,因此如果使用觸發器方法,您可以正常使用它。
規則方法的另一個缺點是,如果規則集未涵蓋插入日期,則沒有簡單的方法可以強制產生錯誤;資料將以靜默方式進入根資料表。
確保 constraint_exclusion 組態參數未在 postgresql.conf
中停用;否則可能會不必要地存取子資料表。
正如我們所看到的,複雜的資料表層級可能需要大量的 DDL。 在上面的範例中,我們將每月建立一個新的子資料表,因此編寫一個自動產生所需 DDL 的腳本可能是明智之舉。
要快速移除舊資料,只需刪除不再需要的子資料表即可:
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;
以下注意事項適用於使用繼承實現的分割:
沒有自動方法可以驗證所有 CHECK
約束是否互斥。 建立程式碼來產生子資料表以及建立和/或修改相關物件,比手動編寫每個物件更安全。
索引和外來鍵約束適用於單個資料表,而不適用於它們的繼承子資料表,因此有一些 注意事項 需要注意。
此處顯示的方案假設列的鍵欄位值永遠不會變更,或者至少變更不足以要求將其移動到另一個分割區。 嘗試執行此操作的 UPDATE
將因 CHECK
約束而失敗。 如果您需要處理這種情況,您可以在子資料表上放置適當的更新觸發器,但這會使結構的管理變得更加複雜。
如果您使用的是手動 VACUUM
或 ANALYZE
命令,請不要忘記您需要在每個子資料表上單獨執行它們。 像這樣的命令:
ANALYZE measurement;
將僅處理根資料表。
具有 ON CONFLICT
子句的 INSERT
陳述式不太可能按預期工作,因為 ON CONFLICT
動作僅在指定的目標關係(而非其子關係)上發生唯一性違規時才會執行。
除非應用程式明確了解分割方案,否則將需要觸發器或規則來將列路由到所需的子資料表。 觸發器可能很難編寫,並且會比宣告式分割在內部執行的元組路由慢得多。
分割區修剪 是一種查詢最佳化技術,可提高宣告式分割資料表的效能。 例如:
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
語句中定義的參數、使用從子查詢獲得的值,或在巢狀迴圈聯結的內側使用參數化值。分割區修剪可以在以下任何時間執行:
在查詢計畫的初始化期間。可以在此處針對在執行初始化階段已知其值的參數執行分割區修剪。在此階段修剪的分割區不會顯示在查詢的 EXPLAIN
或 EXPLAIN ANALYZE
中。可以透過觀察 EXPLAIN
輸出的 “Subplans Removed” 屬性來確定在此階段移除的分割區數量。
在查詢計畫的實際執行期間。也可以在此處執行分割區修剪,以使用僅在實際查詢執行期間已知其值的值來移除分割區。這包括來自子查詢的值和來自執行時期參數的值,例如來自參數化巢狀迴圈聯結的值。由於這些參數的值在查詢執行期間可能會多次更改,因此每當分割區修剪使用的其中一個執行參數發生更改時,就會執行分割區修剪。確定在此階段是否修剪了分割區需要仔細檢查 EXPLAIN ANALYZE
輸出中的 loops
屬性。對應於不同分割區的子計畫對於此屬性可能具有不同的值,具體取決於它們在執行期間被修剪了多少次。如果它們每次都被修剪,則某些可能會顯示為 (never executed)
。
可以使用 enable_partition_pruning 設定停用分割區修剪。
約束排除是一種與分割區修剪類似的查詢優化技術。雖然它主要用於使用傳統繼承方法實現的分割區,但它可用於其他目的,包括宣告式分割區。
約束排除的工作方式與分割區修剪非常相似,除了它使用每個表格的 CHECK
約束條件(因此得名)之外,而分割區修剪則使用表格的分割區邊界,分割區邊界僅在宣告式分割區的情況下存在。另一個區別是約束排除僅在計畫時應用;沒有嘗試在執行時移除分割區。
事實上,約束排除使用 CHECK
約束條件,這使得它比分割區修剪慢,但有時可以用作優勢:因為除了它們的內部分割區邊界之外,甚至可以在宣告式分割表格上定義約束條件,因此約束排除可能能夠從查詢計畫中刪除額外的分割區。
constraint_exclusion 的預設(和建議)設定既不是 on
也不是 off
,而是一個稱為 partition
的中間設定,它會導致該技術僅應用於可能正在處理繼承分割區表格的查詢。on
設定會導致規劃器檢查所有查詢中的 CHECK
約束條件,即使是簡單的且不太可能受益的查詢也是如此。
以下注意事項適用於約束排除:
約束排除僅在查詢規劃期間應用,這與分割區修剪不同,分割區修剪也可以在查詢執行期間應用。
約束排除僅在查詢的 WHERE
子句包含常數(或外部提供的參數)時才有效。例如,與非不可變函數(例如 CURRENT_TIMESTAMP
)的比較無法優化,因為規劃器無法知道該函數的值在執行時可能屬於哪個子表格。
保持分割區約束條件簡單,否則規劃器可能無法證明不需要訪問子表格。對於清單分割區使用簡單的相等條件,或對於範圍分割區使用簡單的範圍測試,如前面的範例所示。一個好的經驗法則是分割區約束條件應僅包含分割區欄位與常數使用 B-tree 索引運算子進行比較的比較,因為只有 B-tree 索引欄位才允許在分割區鍵中使用。
在約束排除期間會檢查父表格的所有子表格上的所有約束條件,因此大量的子表格可能會大大增加查詢規劃時間。因此,基於傳統繼承的分割區將與多達一百個子表格一起良好工作;不要嘗試使用數千個子表格。
應仔細選擇如何分割表格,因為查詢規劃和執行的效能可能會受到不良設計的負面影響。
最關鍵的設計決策之一將是您分割資料所依據的欄位或欄位集。通常,最佳選擇是按欄位或欄位集進行分割,這些欄位或欄位集最常出現在對分割表格執行的查詢的 WHERE
子句中。與分割區邊界約束條件相容的 WHERE
子句可用於修剪不需要的分割區。但是,您可能被迫根據 PRIMARY KEY
或 UNIQUE
約束條件的要求做出其他決定。在規劃分割區策略時,移除不需要的資料也是一個需要考慮的因素。可以相當快地分離整個分割區,因此以這樣的方式設計分割區策略可能是有益的,即所有要同時移除的資料都位於單個分割區中。
選擇表格應劃分的目標分割區數量也是一個需要做出的關鍵決策。沒有足夠的分割區可能意味著索引仍然太大,並且資料局部性仍然很差,這可能導致低快取命中率。但是,將表格劃分為太多分割區也會導致問題。太多的分割區可能意味著更長的查詢規劃時間以及查詢規劃和執行期間更高的記憶體消耗,如下面進一步描述的那樣。在選擇如何分割表格時,也請務必考慮未來可能發生的變化。例如,如果您選擇每個客戶一個分割區,並且您目前擁有少數大型客戶,請考慮如果幾年後您發現自己擁有大量小型客戶的影響。在這種情況下,最好選擇按 HASH
進行分割,並選擇合理數量的分割區,而不是嘗試按 LIST
進行分割,並希望客戶數量不會增加到超出實際分割資料的範圍。
子分割可用於進一步分割預期會比其他分割區更大的分割區。 另一種選擇是在分割區鍵中使用多個欄位的範圍分割。 這兩種方法都可能容易導致過多的分割區數量,因此建議加以節制。
在查詢規劃和執行期間,務必考慮分割區的額外負擔。 查詢規劃器通常能夠很好地處理具有高達數千個分割區的分割區層級結構,前提是典型的查詢允許查詢規劃器修剪掉除了少數分割區之外的所有分割區。 當規劃器執行分割區修剪後,剩餘的分割區越多,規劃時間就越長,記憶體消耗也就越高。 另一個需要關注擁有大量分割區的原因是,伺服器的記憶體消耗可能會隨著時間的推移而顯著增加,特別是當許多工作階段存取大量分割區時。 這是因為每個分割區都需要將其metadata載入到每個存取它的工作階段的本地記憶體中。
對於資料倉儲類型的工作負載,使用比OLTP類型工作負載更多的分割區是有意義的。 一般來說,在資料倉儲中,查詢規劃時間不太重要,因為大部分的處理時間都花在查詢執行上。 對於這兩種工作負載類型,及早做出正確的決策非常重要,因為重新分割大量資料可能會非常緩慢。 模擬預期的工作負載通常有助於最佳化分割區策略。 切勿僅僅假設更多的分割區比更少的分割區更好,反之亦然。
如果您在文件中發現任何不正確、與您使用特定功能的經驗不符或需要進一步澄清的內容,請使用此表格回報文件問題。