PostgreSQL LIST、RANGE 表分區的實現方案
簡 介
PG分區:就是把邏輯上的一個大表分割成物理上的幾塊。
分區的優點
1. 某些類型的查詢性能得到提升
2. 更新的性能也可以得到提升,因為某塊的索引要比在整個數據集上的索引要小。
3. 批量刪除可以通過簡單的刪除某個分區來實現。
4. 可以將很少用的數據移動到便宜的、轉速慢的存儲介質上。
分區實現原理
10.x版本之前PG表分區的實現原理:PG中是通過表的繼承來實現的,建立一個主表,裡面是空的,然後每個分區去繼承它。無論何時,該主表裡面都必須是空的
官網建議:隻有當表本身大小超過瞭機器物理內存的實際大小時,才考慮分區。
原分區用法
以繼承表的方式實現:
create table tbl( a int, b varchar(10) ); create table tbl_1 ( check ( a <= 1000 ) ) INHERITS (tbl); create table tbl_2 ( check ( a <= 10000 and a >1000 ) ) INHERITS (tbl); create table tbl_3 ( check ( a <= 100000 and a >10000 ) ) INHERITS (tbl);
再通過創建觸發器或者規則,實現數據分發,隻需要向子表插入數據則會自動分配到子表中
CREATE OR REPLACE FUNCTION tbl_part_tg() RETURNS TRIGGER AS $$ BEGIN IF ( NEW. a <= 1000 ) THEN INSERT INTO tbl_1 VALUES (NEW.*); ELSIF ( NEW. a > 1000 and NEW.a <= 10000 ) THEN INSERT INTO tbl_2 VALUES (NEW.*); ELSIF ( NEW. a > 10000 and NEW.a <= 100000 ) THEN INSERT INTO tbl_3 VALUES (NEW.*); ELSIF ( NEW. a > 100000 and NEW.a <= 1000000 ) THEN INSERT INTO tbl_4 VALUES (NEW.*); ELSE RAISE EXCEPTION 'data out of range!'; END IF; RETURN NULL; END; $$ LANGUAGE plpgsql; CREATE TRIGGER insert_tbl_part_tg BEFORE INSERT ON tbl FOR EACH ROW EXECUTE PROCEDURE tbl_part_tg();
分區創建成功
如何實現分區過濾?
對於分區表來說,如果有50個分區表,對於某個條件的值如果能確定,那麼很可能直接過濾掉49個分區,大大提高掃描速度,當然分區表也能放在不同的物理盤上,提高IO速度。
對於查詢是怎麼實現分區表過濾呢?
約束排除 是否使用約束排除通過postgresql.conf中參數constraint_exclusion 來控制,
隻有三個值
constraint_exclusion = on
on:所有情況都會進行約束排除檢查
off:關閉,所有約束都不生效
partition:對分區表或者繼承表進行約束排查,默認為partition
如:
select *from tbl where a = 12345;
首先找到主表tbl,然後通過tbl找到它的子表,找到後再對再拿著謂詞條件a = 12345對一個個子表約束進行檢查,不符合條件表就去掉不掃描,實現分區表過濾,下面簡單介紹下約束排除源碼邏輯。
如何實現數據分發?
基於規則的話,會在查詢重寫階段按時替換規則生成新的插入語句,基於觸發器會在insert主表前觸發另外一個insert操作,這兩個邏輯都比較簡單,相關代碼不再介紹。
錯誤描述:在新建分區主表時提示以下錯誤信息
錯誤原因:在本地postgresql.conf 配置瞭 search_path = ‘$user’ ,所以在使用的時候需要先創建當前用戶對應的schema,如果不存在,則會提示錯誤
解決方法:在創建表時指定創建的schemal,即可成功。
PostgreSQL 10.x LIST分區方案
postgres=# CREATE TABLE list_parted ( postgres(# a int postgres(# ) PARTITION BY LIST (a); CREATE TABLE postgres=# CREATE TABLE part_1 PARTITION OF list_parted FOR VALUES IN (1); CREATE TABLE postgres=# CREATE TABLE part_2 PARTITION OF list_parted FOR VALUES IN (2); CREATE TABLE postgres=# CREATE TABLE part_3 PARTITION OF list_parted FOR VALUES IN (3); CREATE TABLE postgres=# CREATE TABLE part_4 PARTITION OF list_parted FOR VALUES IN (4); CREATE TABLE postgres=# CREATE TABLE part_5 PARTITION OF list_parted FOR VALUES IN (5); CREATE TABLE postgres=# postgres=# insert into list_parted values(32); --faled ERROR: no partition of relation "list_parted" found for row DETAIL: Failing row contains (32). postgres=# insert into part_1 values(1); INSERT 0 1 postgres=# insert into part_1 values(2);--faled ERROR: new row for relation "part_1" violates partition constraint DETAIL: Failing row contains (2). postgres=# explain select *from list_parted where a =1; QUERY PLAN ----------------------------------------------------------------- Append (cost=0.00..41.88 rows=14 width=4) -> Seq Scan on list_parted (cost=0.00..0.00 rows=1 width=4) Filter: (a = 1) -> Seq Scan on part_1 (cost=0.00..41.88 rows=13 width=4) Filter: (a = 1) (5 rows)
上面是LIST分區表,建表是先建主表,再建子表,子表以 PARTITION OF 方式說明和主表關系,約束條件應該就是後面的in裡面。
Explain 執行sql解析計劃
cost:數據庫自定義的消耗單位,通過統計信息來估計SQL消耗。(查詢分析是根據analyze的固執生成的,生成之後按照這個查詢計劃執行,執行過程中analyze是不會變的。所以如果估值和真是情況差別較大,就會影響查詢計劃的生成。)
rows:根據統計信息估計SQL返回結果集的行數。
width:返回結果集每一行的長度,這個長度值是根據pg_statistic表中的統計信息來計算的。
PostgreSQL 10.x RANGE分區
創建RANGE分區
postgres=# CREATE TABLE range_parted ( postgres(# a int postgres(# ) PARTITION BY RANGE (a); CREATE TABLE postgres=# CREATE TABLE range_parted1 PARTITION OF range_parted FOR VALUES from (1) TO (1000); CREATE TABLE postgres=# CREATE TABLE range_parted2 PARTITION OF range_parted FOR VALUES FROM (1000) TO (10000); CREATE TABLE postgres=# CREATE TABLE range_parted3 PARTITION OF range_parted FOR VALUES FROM (10000) TO (100000); CREATE TABLE postgres=# postgres=# insert into range_parted1 values(343); INSERT 0 1 postgres=# postgres=# explain select *from range_parted where a=32425; QUERY PLAN --------------------------------------------------------------------- Append (cost=0.00..41.88 rows=14 width=4) -> Seq Scan on range_parted (cost=0.00..0.00 rows=1 width=4) Filter: (a = 32425) -> Seq Scan on range_parted3 (cost=0.00..41.88 rows=13 width=4) Filter: (a = 32425) (5 rows) postgres=# set constraint_exclusion = off; SET postgres=# explain select *from range_parted where a=32425; QUERY PLAN --------------------------------------------------------------------- Append (cost=0.00..125.63 rows=40 width=4) -> Seq Scan on range_parted (cost=0.00..0.00 rows=1 width=4) Filter: (a = 32425) -> Seq Scan on range_parted1 (cost=0.00..41.88 rows=13 width=4) Filter: (a = 32425) -> Seq Scan on range_parted2 (cost=0.00..41.88 rows=13 width=4) Filter: (a = 32425) -> Seq Scan on range_parted3 (cost=0.00..41.88 rows=13 width=4) Filter: (a = 32425) (9 rows)
上述操作中的 a的取值范圍為【0,1000)即插入值若為1000邊界值,則會保存在第二個分區表中和LIST差不多,就是語法略有不同,范圍表值是一個連續的范圍,LIST表是單點或多點的集合。
從上面例子可以看到,顯然還是走的約束排除過濾子表的方式。
constraint_exclusion = “on ,off,partition ”; 該參數為postgresql.conf中的參數
on
表示所有的查詢都會執行約束排除
off
關閉,所有的查詢都不會執行約束排除
partition
:表示隻對分區的表進行約束排除
分區列的類型必須支持btree索引接口(幾乎涵蓋所有類型, 後面會說到檢查方法)。
更新後的數據如果超出瞭所在分區的范圍,則會報錯
PostgreSQL 分區註意事項
語法
1、創建主表
[ PARTITION BY { RANGE | LIST } ( { column_name | ( expression ) } [ COLLATE collation ] [ opclass ] [, ... ] ) ]
2、創建分區
PARTITION OF parent_table [ ( { column_name [ column_constraint [ ... ] ] | table_constraint } [, ... ] ) ] FOR VALUES partition_bound_spec and partition_bound_spec is: { IN ( expression [, ...] ) -- list分區 | FROM ( { expression | UNBOUNDED } [, ...] ) TO ( { expression | UNBOUNDED } [, ...] ) } -- range分區, unbounded表示無限小或無限大
語法解釋
partition by
指定分區表的類型range或list指定分區列,或表達式作為分區鍵。
range
分區表鍵:支持指定多列、或多表達式,支持混合(鍵,非表達式中的列,會自動添加not null的約束)
list
分區表鍵:支持單個列、或單個表達式
分區鍵必須有對應的btree索引方法的ops(可以查看系統表得到)
select typname from pg_type where oid in (select opcintype from pg_opclass);
主表不會有任何數據,數據會根據分區規則進入對應的分區表
如果插入數據時,分區鍵的值沒有匹配的分區,會報錯
不支持全局的unique, primary key, exclude, foreign key約束,隻能在對應的分區建立這些約束
分區表和主表的 列數量,定義 必須完全一致,(包括OID也必須一致,要麼都有,要麼都沒有)
可以為分區表的列單獨增加Default值,或約束。
用戶還可以對分區表增加表級約束
如果新增的分區表check約束,名字與主表的約束名一致,則約束內容必須與主表一致
當用戶往主表插入數據庫時,記錄被自動路由到對應的分區,如果沒有合適的分區,則報錯
如果更新數據,並且更新後的KEY導致數據需要移動到另一分區,則會報錯,(意思是分區鍵 可以更新,但是不支持更新後的數據移出到別的分區表)
修改主表的字段名,字段類型時,會自動同時修改所有的分區
TRUNCATE 主表時,會清除所有繼承表分區的記錄(如果有多級分區,則會一直清除到所有的直接和間接繼承的分區)
如果要清除單個分區,請對分區進行操作
如果要刪除分區表,可以使用DROP TABLE的DDL語句,註意這個操作會對主表也加access exclusive lock。
補充:對PostgreSQL語法分析中 targetlist 的理解
在 gram.y 中:
simple_select: SELECT opt_distinct target_list into_clause from_clause where_clause group_clause having_clause window_clause { SelectStmt *n = makeNode(SelectStmt); n->distinctClause = $2; n->targetList = $3; n->intoClause = $4; n->fromClause = $5; n->whereClause = $6; n->groupClause = $7; n->havingClause = $8; n->windowClause = $9; $$ = (Node *)n; } ……
把它修改一下,增加:
simple_select: SELECT opt_distinct target_list into_clause from_clause where_clause group_clause having_clause window_clause { SelectStmt *n = makeNode(SelectStmt); n->distinctClause = $2; n->targetList = $3; n->intoClause = $4; n->fromClause = $5; n->whereClause = $6; n->groupClause = $7; n->havingClause = $8; n->windowClause = $9; $$ = (Node *)n; fprintf(stderr,"length of list: %d\n", n->targetList->length); } ……
psql 中執行:
select id, name from a8;
後臺出現:
length of list: 2
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。如有錯誤或未考慮完全的地方,望不吝賜教。
推薦閱讀:
- postgresql如何找到表中重復數據的行並刪除
- postgresql 刪除重復數據的幾種方法小結
- postgresql insert into select無法使用並行查詢的解決
- 淺談pg_hint_plan定制執行計劃
- PostgreSQL12.5中分區表的一些操作實例