暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

PostgreSQL传统分区表

原创 梧桐 2023-08-17
418

概述

postgres分区表是将大型表划分为较小的部分,以提高查询性能、优化批量操作,更有效地管理存储,当表的大小超过数据库服务器的物理内存,查询集中在特定数据子集,需要频繁进行批量操作,以及有明确生命周期的数据(如历史数据或过期数据)考虑使用分区表。

postgres支持两种分区表,传统分区表与内置分区表,传统分区表是通过继承和触发器方式实现的,我们先了解下继承表:

继承表

创建父表、子表

create table tbl_log(id int4, create_date date,log_type text) ; create table tbl_log_sql(sql text) inherits(tbl_log); /*inherits(tbl_log)表示表tbl_log_sql继承表tbl_log*/

子表定义了额外字段sql, 其他字段继承父表tbl_log, 查看tbl_log_sql表结构:

\d tbl_log_sql Table "public.tbl_log_sql" Column | Type | Collation | Nullable | Default -------------+---------+-----------+----------+--------- id | integer | | | create_date | date | | | log_type | text | | | sql | text | | | Inherits: tbl_log /*表示继承了表tbl_log*/

子表比父表多了一个字段:sql

子表父表在应用的数据情况对比

父表和子表都可以插入数据,接着分别在父表和子表中插入一条数据:

insert into tbl_log values(1,'2020-08-26',null); INSERT 0 1 postgres=# select * from tbl_log_sql; id | create_date | log_type | sql ----+-------------+----------+----- (0 rows)

父表插入数据,子表不变;

insert into tbl_log_sql values(2,'2020-08-27',null,'select version()');
select * from tbl_log;
 id | create_date | log_type 
----+-------------+----------
  1 | 2020-08-26  | 
  2 | 2020-08-27  | 
(2 rows)

子表插入数据库,父表随之变化;

可见父表操作不影响子表,子表操作父表随之变化。

确定数据来源

查看表oid:

select tableoid,* from tbl_log ; tableoid | id | create_date | log_type ----------+----+-------------+---------- 91336 | 1 | 2017-08-26 | 91342 | 2 | 2020-08-27 | (2 rows)

找表名:

select p.relname,c.* from tbl_log c,pg_class p where c.tableoid =p.oid;

只查询父表

在父表名称前加上only:

select * from only tbl_log; 

更新、 删除父表

如果父表名称前没有加only, 则会对父表和所有子表进行操作

delete from tbl_log; select count(*) from tbl_log; select * from tbl_log_sql;

都被删除了吧。

使用了继承表的的update、delete的操作需特别谨慎,小心数据被连带删除。您可以使用以下命令查看是否有继承表。

列出所有父表

SELECT nspname , relname , COUNT(*) AS partition_num FROM pg_class c , pg_namespace n , pg_inherits i WHERE c.oid = i.inhparent AND c.relnamespace = n.oid AND c.relhassubclass AND c.relkind = 'r' GROUP BY 1,2 ORDER BY partition_num DESC;

传统分区表

创建传统分区表

第一步:创建父表,如果父表上定义了约束,子表会继承,因此除非是全局约束,否则不应该在父表上定义约束,另外,父表不应该写入数据。

create table log_ins(id serial,user_id int4,create_time timestamp(0) without time zone);

第二步:创建子表(继承表或分区),字段定义与父表保持一致。13个表

create table log_ins_history(check(create_time <'2020-01-01')) inherits (log_ins); create table log_ins_202001(check(create_time >='2020-01-01' and create_time <'2020-02-01')) inherits (log_ins); create table log_ins_202002(check(create_time >='2020-02-01' and create_time <'2020-03-01')) inherits (log_ins); create table log_ins_202003(check(create_time >='2020-03-01' and create_time <'2020-04-01')) inherits (log_ins); create table log_ins_202004(check(create_time >='2020-04-01' and create_time <'2020-05-01')) inherits (log_ins); create table log_ins_202005(check(create_time >='2020-05-01' and create_time <'2020-06-01')) inherits (log_ins); create table log_ins_202006(check(create_time >='2020-06-01' and create_time <'2020-07-01')) inherits (log_ins); create table log_ins_202007(check(create_time >='2020-07-01' and create_time <'2020-08-01')) inherits (log_ins); create table log_ins_202008(check(create_time >='2020-08-01' and create_time <'2020-09-01')) inherits (log_ins); create table log_ins_202009(check(create_time >='2020-09-01' and create_time <'2020-10-01')) inherits (log_ins); create table log_ins_202010(check(create_time >='2020-10-01' and create_time <'2020-11-01')) inherits (log_ins); create table log_ins_202011(check(create_time >='2020-11-01' and create_time <'2020-12-01')) inherits (log_ins); create table log_ins_202012(check(create_time >='2020-12-01' and create_time <='2020-12-31')) inherits (log_ins);

第三步:子表创建约束,满足约束条件的数据才能写入对应子表,子表约束值范围不要有重叠。
第四步:子表创建索引,子表不继承父表索引,子表索引需要手工创建。

CREATE INDEX idx_his_ctime ON log_ins_history USING btree(create_time); CREATE INDEX idx_log_ins_202001_ctime ON log_ins_202001 USING btree(create_time); CREATE INDEX idx_log_ins_202002_ctime ON log_ins_202002 USING btree(create_time); CREATE INDEX idx_log_ins_202003_ctime ON log_ins_202003 USING btree(create_time); CREATE INDEX idx_log_ins_202004_ctime ON log_ins_202004 USING btree(create_time); CREATE INDEX idx_log_ins_202005_ctime ON log_ins_202005 USING btree(create_time); CREATE INDEX idx_log_ins_202006_ctime ON log_ins_202006 USING btree(create_time); CREATE INDEX idx_log_ins_202007_ctime ON log_ins_202007 USING btree(create_time); CREATE INDEX idx_log_ins_202008_ctime ON log_ins_202008 USING btree(create_time); CREATE INDEX idx_log_ins_202009_ctime ON log_ins_202009 USING btree(create_time); CREATE INDEX idx_log_ins_202010_ctime ON log_ins_202010 USING btree(create_time); CREATE INDEX idx_log_ins_202011_ctime ON log_ins_202011 USING btree(create_time); CREATE INDEX idx_log_ins_202012_ctime ON log_ins_202012 USING btree(create_time);

第五步:[可选] 在父表上定义INSERT、 DELETE、 UPDATE触发器,将SQL分发到对应子表,因为应用可以根据分区规则定位到对应分区进行DML操作。

父表上不存储数据,可以不用在父表上创建索引。

创建触发器函数,设置数据插入父表时的路由规则,

CREATE OR REPLACE FUNCTION log_ins_insert_trigger() RETURNS trigger LANGUAGE plpsql AS $function$ BEGIN IF (NEW.create_time <'2020-01-01') THEN INSERT INTO log_ins_history VALUES (NEW.*); ELSIF (NEW.create_time>='2020-01-01' and NEW.create_time <'2020-02-01') THEN INSERT INTO log_ins_202001 VALUES (NEW.*); ELSIF (NEW.create_time>='2020-02-01' and NEW.create_time<'2020-03-01') THEN INSERT INTO log_ins_202002 VALUES (NEW.*); ELSIF (NEW.create_time>='2020-03-01' and NEW.create_time<'2020-04-01') THEN INSERT INTO log_ins_202003 VALUES (NEW.*); ELSIF (NEW.create_time>='2020-04-01' and NEW.create_time<'2020-05-01') THEN INSERT INTO log_ins_202004 VALUES (NEW.*); ELSIF (NEW.create_time>='2020-05-01' and NEW.create_time<'2020-06-01') THEN INSERT INTO log_ins_202005 VALUES (NEW.*); ELSIF (NEW.create_time>='2020-06-01' and NEW.create_time<'2020-07-01') THEN INSERT INTO log_ins_202006 VALUES (NEW.*); ELSIF (NEW.create_time>='2020-07-01' and NEW.create_time<'2020-08-01') THEN INSERT INTO log_ins_202007 VALUES (NEW.*); ELSIF (NEW.create_time>='2020-08-01' and NEW.create_time<'2020-09-01') THEN INSERT INTO log_ins_202008 VALUES (NEW.*); ELSIF (NEW.create_time>='2020-09-01' and NEW.create_time<'2020-10-01') THEN INSERT INTO log_ins_202009 VALUES (NEW.*); ELSIF (NEW.create_time>='2020-10-01' and NEW.create_time<'2020-11-01') THEN INSERT INTO log_ins_202012 VALUES (NEW.*); ELSIF (NEW.create_time>='2020-11-01' and NEW.create_time<'2020-12-01') THEN INSERT INTO log_ins_202011 VALUES (NEW.*); ELSIF (NEW.create_time >='2020-12-01' and NEW.create_time <='2020-12-31') THEN INSERT INTO log_ins_202012 VALUES (NEW.*); ELSE RAISE EXCEPTION 'create_time out of range. Fix the log_ins_insert_trigger() function'; END IF; RETURN NULL; END; $function$;

在父表上定义插入触发器

CREATE TRIGGER insert_log_ins_trigger BEFORE INSERT ON log_ins FOR EACH ROW 
EXECUTE PROCEDURE log_ins_insert_trigger(); 

第六步:设置constraint_exclusion参数设为on,如果为off, 父表上的SQL性能会降低。

内置分区表执行计划受constraint exclusion参数影响,关闭此参数后,根据分区键查询时执行计划不会定位到相应分区。 先在会话级关闭此参数。

constraint_exclusion是表分区策略,意味着检查约束放在了表上,以限制可以在表中插入哪种数据。

SET constraint_exclusion =off;

不建议设置成on,优化器通过检查约束来优化查询的方式本身就带来一定开销,如果所有表都启用这个特性,会加重优化器负担。

触发器创建完成后,往父表log_ins插人数据时,会执行触发器并触发函数log_ins_insert_trigger()表数据插入到相应分区中。 DELETE、 UPDATE触发器和函数创建过程和INSERT方式类似,不再列出,这步完成之后,传统分区表的创建步骤已全部完成。

使用分区表

插入数据库

INSERT INTO log_ins(user_id,create_time) SELECT round(1000000000*random()),generate_series('2019-12-01'::date,'2020-12-31'::date,'1 minute');

查询数据

select count(*) from log_ins;
 count  
--------
 570241
(1 row)
select * from only log_ins;
 id | user_id | create_time 
----+---------+-------------
(0 rows)
select count(*) from  log_ins_202001;
 count 
-------
 44640
(1 row)

查看各表情况

\dt+  log_ins* 
                        List of relations
 Schema |      Name       | Type  | Owner |  Size   | Description 
--------+-----------------+-------+-------+---------+-------------
 public | log_ins         | table | postgres  | 0 bytes | 
 public | log_ins_202001  | table | postgres  | 2016 kB | 
 public | log_ins_202002  | table | postgres  | 1920 kB | 
 public | log_ins_202003  | table | postgres  | 2016 kB | 
 public | log_ins_202004  | table | postgres  | 1984 kB | 
 public | log_ins_202005  | table | postgres  | 2016 kB | 
 public | log_ins_202006  | table | postgres  | 1984 kB | 
 public | log_ins_202007  | table | postgres  | 2016 kB | 
 public | log_ins_202008  | table | postgres  | 2016 kB | 
 public | log_ins_202009  | table | postgres  | 1984 kB | 
 public | log_ins_202010  | table | postgres  | 2016 kB | 
 public | log_ins_202011  | table | postgres  | 1984 kB | 
 public | log_ins_202012  | table | postgres  | 1984 kB | 
 public | log_ins_history | table | postgres  | 2016 kB | 
(14 rows)

父表无数据,各子表一切正常,传统分区表情况介绍到这儿。

添加分区

添加分区属于分区表维护的常规操作之一,比如历史表范围分区到期之前需要扩分区,log_ins表为日志表,每个分区存储当月数据,假如分区’快到期了,可通过以下SQL扩分区,首先创建子表

CREATE TABLE log_ins_202101(CHECK(create_time>='2021-01-01' and create_time 
<'2021-02-01')) INHERITS (log_ins);

通常会多定义一些分区,这个操作要根据具体场景来进行。
之后创建相关索引

CREATE INDEX idx_log_ins_20210l_ctime ON log_ins_202101 USING btree(create_time); 

然后刷新触发器函数log_ins_ i nsert_ trigger(),添加相应代码,将符合路由规则的数据
插入新分区,详见之前定义的这个函数,这步完成后,添加分区操作完成,可通过\d+ log_ins命令查看log_ins 的所有分区。
这种方法比较直接,创建分区时就将分区继承到父表,如果中间步骤有错可能对生产
系统带来影响,比较推荐的做法是将以上操作分解成以下几个步骤,降低对生产系统的影

一创建分区

create table log_ins_202102(like log_ins including all); 

一添加约束

alter table log_ins_202102 add constraint log_ins_202102_create_time_check 
check(create_time >='2021-02-01' and create_time<'2021-03-01');

一刷新触发器函数log_ins_insert_trigger()

	ELSIF (NEW.create_time >='2021-01-01' and NEW.create_time<='2021-02-01') THEN 
		INSERT INTO log_ins_202101 VALUES (NEW.*); 	

函数刷新前建议先备份函数代码。

--所有步骤完成后,将新分区log_ins_202102继承到父表log_ins

ALTER TABLE log_ins_202102 INHERIT log_ins;

以上方法是将新分区所有操作完成后,再将分区继承到父表,降低了生产系统添加分区操作的风险,当然,在生产系统添加分区前建议在测试环境事先演练一把。

查看一下

\d+ log_ins Table "public.log_ins" Column | Type | Collation | Nullable | Default | Storage | Stats target | Description -------------+--------------------------------+-----------+----------+------------------------------------- id | integer | | not null | nextval('log_ins_id_seq'::regclass) | plain | | user_id | integer | | | | plain | | create_time | timestamp(0) without time zone | | | | plain | | Triggers: insert_log_ins_trigger BEFORE INSERT ON log_ins FOR EACH ROW EXECUTE PROCEDURE log_ins_insert_trigger() Child tables: log_ins_202001, log_ins_202002, log_ins_202003, log_ins_202004, log_ins_202005, log_ins_202006, log_ins_202007, log_ins_202008, log_ins_202009, log_ins_202010, log_ins_202011, log_ins_202012, log_ins_202101, log_ins_202102, log_ins_history

删除分区

分区表的一个重要优势是对于大表的管理上十分方便,例如需要删除历史数据时可以直接删除一个分区,这比DELETE方式效率高了多个数量级,传统分区表删除分区通常有两种方法,第一种方法是直接删除分区

DROP TABLE log ins_202102

就像删除普通表一样删除分区即可,当然删除分区前需再三确认是否需要备份数据;
另一种比较推荐的删除分区方法是先将分区的继承关系去掉

ALTER TABLE log ins 202102 NO INHERIT log ins;

执行以上命令后,log_ins_ 202102分区不再属于分区表log_ins的分区,但log_ins_202102表依然保留可供查询,这种方式相比方法一提供了一个缓冲时间,属于比较稳妥的删除分区方法,因为在拿掉子表继承关系后,只要没删除这个子表,还可以使子表重新继承父表。

注意事项

  1. 当往父表上插入数据时,需事先在父表上创建路由函数和触发器,数据才会根据分区键路由规则插入到对应分区中,目前仅支持范围分区和列表分区。

  2. 分区表上的索引、约束需要使用单独的命令创建,目前没有办法一次性自动在所有分区上创建索引、约束。

  3. 父表和子表允许单独定义主键,因此父表和子表可能存在重复的主键记录,目前不支持在分区表上定义全局主键。

  4. UPDATE时不建议更新分区键数据,特别是会使数据从一个分区移动到另一分区的场景,可通过更新触发器实现,但会带来管理上的成本。

  5. 性能方面:根据本节的测试数据和测试场景,传统分区表根据非分区键查询相比普通表性能差距较大,因为这种场景下分区表会扫描所有分区;根据分区键查询相比普通表性能有小幅降低,而查询分区表子表性能相比普通表略有提升;使用分区表不一定能提升性能,如果业务模型90%(估算的百分比,意思是大部分)以上的操作都能基于分区健操作,并且SQL可以定位到子表,这时建议使用分区表。

最后修改时间:2025-01-21 11:22:16
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论