原文:https://zhuanlan.zhihu.com/p/116589705
PostgreSQL 有兩種父、子表關系:分區(partition)和繼承(inherit)。在PostgreSQL中,表分區是內置聲明式分區(built-in built-in declarative partitioning),適用於大部分常見用例;另外通過表繼承也能實現表分區,而且具有一些聲明式分區不具備的特性。
注意:內置聲明式分區表(built-in declaratively patitioned table),在不同的地方可能會有不同的叫法,總體上:原生分區 = 內置分區表 = 聲明式分區表 = 分區表。
一、內置聲明式表分區
表的分區就是將一個邏輯上的大表(主要指數據量大),切分為多個小的物理的分片。
分區的優點:
1 在某些情況下,尤其是當表中大多數被頻繁訪問的行位於單個分區或少量分區中時,查詢性能可以得到顯着提高。分區替代了索引的前幾列,從而減小了索引的大小,並使索引中頻繁使用的部分更有可能裝入內存。
2 當查詢或更新訪問單個分區的很大一部分時,可以通過對該分區進行順序掃描而不是使用索引和分散在整個表中的隨機訪問讀取,來提高性能。
直接從分區表查詢數據比從一個大而全的全量數據表中讀取數據效率更高。
3 如果計划將這種需求計划到分區設計中,則可以通過添加或刪除分區來完成批量加載和刪除。使用ALTER TABLE DETACH PARTITION或使用DROP TABLE刪除單個分區比批量操作要快得多。這些命令還完全避免了由批量DELETE引起的VACUUM開銷。
數據維護成本降低。比如:某一部分數據失效,不需要執行命令來更新數據,可以直接解綁指定關系,解除綁定的數據和分區表都依然保留,需要時可以隨時恢復綁定。通過 Flyway 等數據腳本管理能夠方便的控制數據維護,避免人為直接操作數據。
4 很少使用的數據可以遷移到更廉價、更遲緩的存儲介質上。
一個表只能放在一個物理空間上,使用分區表之后可以將不同的表放置在不同的物理空間上,從而達到冷數據放在廉價的物理機器上,熱點數據放置在性能強勁的機器上。
通常只有在表很大的情況下,這些好處才是值得的。表可以從分區中受益的確切時間取決於應用程序,盡管經驗法則是表的大小應超過數據庫服務器的物理內存。
分區的使用限制:
1 無法創建跨所有分區的排除約束。只能單獨約束每個葉子分區。
2 因為PostgreSQL只能在每個分區中單獨進行唯一性約束;因此,分區表上的唯一約束必須包括所有分區鍵列。
3 如果需要 BEFORE ROW 觸發器,則必須定義在單個分區(而不是分區父表)。
4 不允許在同一分區樹中混合臨時和永久關系。因此,如果父表是永久性的,則其分區也必須是永久性的;如果父表是臨時的,則其分區也必須是臨時的。當使用臨時關系時,分區樹的所有成員必須來自同一會話。
3種分區策略:
PostgreSQL內置支持以下3種方式的分區:
- 范圍(Range )分區:表被划分為由鍵列或列集定義的“范圍”,分配給不同分區的值的范圍之間沒有重疊。例如:可以按日期范圍或特定業務對象的標識符范圍,來進行分區。
- 列表(List)分區:通過顯式列出哪些鍵值出現在每個分區中來對表進行分區。
- 哈希(Hash)分區:(自PG11才提供HASH策略)通過為每個分區指定模數和余數來對表進行分區。每個分區將保存行,分區鍵的哈希值除以指定的模數將產生指定的余數。
分區的創建和使用
PostgreSQL提供了將一個表切分為多個片的方法,這些片叫做分區,被切分的表稱作被分區表(這里簡稱父表)。該規范包括分區方法和用作分區鍵的列或表達式列表。
創建分區的方法總結:
1 創建父表:指定分區鍵字段、分區策略(RANGE | LIST | HASH);
2 創建分區:指定父表、分區鍵范圍(分區鍵范圍重疊之后會直接報錯)或DEFAULT;
3 在分區上創建索引:通常,分區鍵字段上的索引是必須的。
創建父表:
CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] table_name ( [
{ column_name data_type [ COLLATE collation ] [ column_constraint [ ... ] ]
| table_constraint
| LIKE source_table [ like_option ... ] }
[, ... ]
] )
[ INHERITS ( parent_table [, ... ] ) ]
[ PARTITION BY { RANGE | LIST | HASH } ( { column_name | ( expression ) } [ COLLATE collation ] [ opclass ] [, ... ] ) ]
...
CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] table_name
OF type_name [ (
{ column_name [ WITH OPTIONS ] [ column_constraint [ ... ] ]
| table_constraint }
[, ... ]
) ]
[ PARTITION BY { RANGE | LIST | HASH } ( { column_name | ( expression ) } [ COLLATE collation ] [ opclass ] [, ... ] ) ]
...
創建分區:
CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] table_name
PARTITION OF parent_table [ (
{ column_name [ WITH OPTIONS ] [ column_constraint [ ... ] ]
| table_constraint }
[, ... ]
) ] { FOR VALUES partition_bound_spec | DEFAULT }
[ PARTITION BY { RANGE | LIST | HASH } ( { column_name | ( expression ) } [ COLLATE collation ] [ opclass ] [, ... ] ) ]
....
partition_bound_spec is:
IN ( partition_bound_expr [, ...] ) |
FROM ( { partition_bound_expr | MINVALUE | MAXVALUE } [, ...] )
TO ( { partition_bound_expr | MINVALUE | MAXVALUE } [, ...] ) |
WITH ( MODULUS numeric_literal, REMAINDER numeric_literal )
父表與分區建立綁定關系
ALTER TABLE [ IF EXISTS ] name
ATTACH PARTITION partition_name { FOR VALUES partition_bound_spec | DEFAULT }
partition_bound_spec is:
IN ( partition_bound_expr [, ...] ) |
FROM ( { partition_bound_expr | MINVALUE | MAXVALUE } [, ...] )
TO ( { partition_bound_expr | MINVALUE | MAXVALUE } [, ...] ) |
WITH ( MODULUS numeric_literal, REMAINDER numeric_literal )
父表與分區解除綁定關系:
ALTER TABLE [ IF EXISTS ] name DETACH PARTITION partition_name;
刪除分區及其中數據(與刪除表方法相同)
DROP TABLE [ IF EXISTS ] partition_name [, ...] [ CASCADE | RESTRICT ]
RESTRICT:缺省選項,如果有任何對象依賴該表則拒絕刪除該表。
CASCADE:自動刪除依賴於表的對象(例如視圖),然后自動刪除依賴於那些對象的所有對象(請參見:5.14 依賴追蹤)
參考:PostgreSQL12 Doc - 5.14 依賴追蹤:
SQL實戰:
1 創建父表
CREATE TABLE measurement (
city_id int not null,
logdate date not null,
peaktemp int,
unitsales int
) PARTITION BY RANGE (logdate);
2 創建分區及子分區
--創建分區
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;
--創建子分區 sub-partition
CREATE TABLE measurement_y2006m02 PARTITION OF measurement_y2006
FOR VALUES FROM ('2006-02-01') TO ('2006-03-01')
PARTITION BY RANGE (peaktemp);
3 創建索引
在父表的鍵字段創建索引,以及創建需要的任何其他索引。 (鍵索引不是嚴格必需的,但是在大多數情況下它是有幫助的。)這將在每個分區上自動創建一個索引,以后創建或附加的任何分區也將包含該索引。
CREATE INDEX ON measurement (logdate);
4 確保 postgresql.conf 中的 enable_partition_pruning 啟用,否則,查詢將不會被優化。
在上面的示例中,我們每個月都會創建一個新分區,因此編寫一個腳本自動生成所需的DDL可能是明智的。
5 維護分區
Analyze measurement;
聲明式分區的最佳實踐
應該謹慎地選擇如何對表進行分區,因為糟糕的設計會對查詢計划和執行的性能產生負面影響。
最關鍵的設計決策之一將是用來划分數據的一列或多列。通常,最好的選擇是按在分區表上執行的查詢的WHERE子句中最常見的列或列集進行分區。與分區鍵匹配並兼容的WHERE子句項可用於修剪不需要的分區。但是,您可能會因對PRIMARY KEY或UNIQUE約束的要求而被迫做出其他決定。在計划分區策略時,刪除不需要的數據也是要考慮的因素。整個分區可以相當快地拆離,因此設計分區策略可能有益於將要立即刪除的所有數據都放在單個分區中。
選擇應將表划分為分區的目標數量也是一個至關重要的決定。沒有足夠的分區可能意味着索引仍然太大,並且數據局部性仍然很差,這可能導致較低的緩存命中率。但是,將表划分為太多分區也會導致問題。過多的分區可能意味着更長的查詢計划時間和更高的內存消耗,同時查詢計划和執行。在選擇如何對表進行分區時,考慮將來可能發生的更改也很重要。例如,如果您選擇為每個客戶分配一個分區,而您目前只有少量大客戶,那么考慮幾年后如果您卻發現自己擁有大量小客戶,將產生什么影響。在這種情況下,最好選擇按HASH分區並選擇合理數量的分區,而不是嘗試按LIST分區,並希望客戶數量不會超出對數據進行分區的實際范圍。
子分區對於進一步划分預期會比其他分區更大的分區很有用,盡管過度的子分區很容易導致大量分區,並可能引起與上段所述的相同問題。
在查詢計划和執行過程中考慮分區的開銷也很重要。查詢計划程序通常能夠很好地處理多達數千個分區的分區層次結構,條件是典型的查詢允許查詢計划程序修剪除少數分區以外的所有分區。在計划者執行分區修剪后,如果剩余更多分區,則計划時間會更長,內存消耗也會更高。對於UPDATE和DELETE命令尤其如此。擔心擁有大量分區的另一個原因是,服務器的內存消耗可能會在一段時間內顯着增長,尤其是在許多會話涉及大量分區的情況下。這是因為每個分區都需要將其元數據加載到與之接觸的每個會話的本地內存中。
對於數據倉庫類型的工作負載,與使用OLTP類型的工作負載相比,使用更多數量的分區是有意義的。通常,在數據倉庫中,由於大多數處理時間是在查詢執行過程中花費的,因此查詢計划時間就不再那么重要了。對於這兩種類型的工作負載中的任何一種,重要的是及早做出正確的決定,因為對大量數據進行重新分區可能會非常緩慢。預期工作負載的模擬通常有助於優化分區策略。永遠不要假設更多的分區比更少的分區更好,反之亦然。
二、通過繼承(inherit)實現表分區
盡管內置的聲明性分區適用於大多數常見用例,但在某些情況下,可能會使用更靈活的方法。可以使用表繼承來實現分區,它具有一些聲明性分區不支持的一些功能,例如:
1 對於聲明性分區,分區必須與父表具有完全相同的列集,而通過表繼承,子表可能具有父級中不存在的額外列。
2 表繼承允許多重繼承。 (一個子表繼承多個父表)
3 聲明性分區僅支持范圍、列表、哈希分區,而表繼承允許按照用戶選擇的方式對數據進行拆分。 (但是請注意,如果約束排除無法有效地修剪子表,則查詢性能可能會很差。)
4 與使用表繼承相比,使用聲明性分區時,某些操作需要更強的鎖定。例如,在分區表中添加分區或從分區表中刪除分區都需要對父表進行ACCESS EXCLUSIVE鎖,而對於常規繼承,SHARE UPDATE EXCLUSIVE鎖就足夠了。
例子:
1 創建要繼承的父表
2 創建多個子表
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 )
要確保約束能保證不同子表中允許的鍵值之間沒有重疊。一個常見的錯誤是設置范圍約束。
例如下面是錯誤的,因為不知道鍵值200屬於哪個子表:
CHECK ( outletID BETWEEN 100 AND 200 )
CHECK ( outletID BETWEEN 200 AND 300 )
下面是好的例子,能夠清晰的看到范圍:
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);
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);
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.*);