PostgreSQL分区表升级-从基于触发器到声明式

1 前言

当数据库中单表的数据量过大时,需要考虑分区来提高性能。对于PostgreSQL数据库的用户而言,如果想在PostgreSQL 9.x 或者更早的版本中使用分区表,你需要用触发器(Trigger)或者规则(Rule),加上检查约束(Check)和继承(Inheritance)机制去实现它。而PostgreSQL 在 10.0版本引入了声明式(declarative)分区。相比于基于“触发器+继承+检查约束” (以下简称基于触发器的方案)和基于“规则+继承+检查约束”的分区方案,声明式分区的在性能有了很大的提升。        

                                    

 

 (援引自平安科技工程师石勇虎的《庖丁解牛之平安vacuum优化之路》)

 

本文将在第二章中介绍声明式分区表的语法,在第三章分别介绍基于触发器的方案和声明式分区表方案,以及它们的应用。在第五章,介绍将基于触发器的方案升级为声明式分区方案的方法。

 

2 声明式分区简介

2.1 声明式分区的语法

以PostgreSQL 11 为例,声明式分区表的最常用的分区形式有两种:

范围分区,该表被划分为由键列或列集定义的“范围”,分配给不同分区的值的范围之间没有重叠。

列表分区,表通过明确列出每个分区中出现的键值进行分区。

 

创建分区表(partitioned table)的语法如下:

CREATE TABLE table_name ( ...)

     [ PARTITION BY { RANGE | LIST }  (  { column_name |  ( expression )  }

 

创建分区(partition)的语法如下:

CREATE TABLE table_name PARTITION OF parent_table [ () ] { FOR VALUES partition_bound_spec | DEFAULT }

 

  其中,

partition_bound_spec是一个表达式。对范围分区,它是:

from (范围的下限)to (范围的上限)

其中,分区范围包含下限而不包含上限。

例如, “from(20)to (30)” 表示取值范围是 [20, 30)

 

对列表分区,它是:

in (value1[, …])

例如:

in (‘a’, ‘b’, ‘c’)

 

2.2 示例

创建范围分区表的示例如下:

create table tb_log (

    id int,

remark varchar(32),

create_time timestamp without time zone

) partition by RANGE(create_time);
 

CREATE TABLE tb_log_default PARTITION OF tb_log DEFAULT;

CREATE TABLE tb_log_201901 PARTITION OF tb_log FOR VALUES FROM ('2019-01-01') TO ('2019-02-01');

CREATE TABLE tb_log_201902 PARTITION OF tb_log FOR VALUES FROM ('2019-02-01') TO ('2019-03-01');

 

创建列表分区的示例如下:

create table tb_student (

    id int,

name varchar(32),

gender int,

classid int,

) partition by list (classid);
 

CREATE TABLE tb_student_default PARTITION OF tb_student DEFAULT;

CREATE TABLE tb_student_1 PARTITION OF tb_student FOR VALUES in ('1');

CREATE TABLE tb_ student_2 PARTITION OF tb_student FOR VALUES in ('2');

 

 

3 分区表方案及案例

我们以事件表 tb_event 为例,介绍基于“触发器+继承+检查约束”的方案和声明式分区方案及其实现案例。它是按月分区的,分区的字段是 utc_event_time,表示 事件发生时刻的UTC 时间,以毫秒为单位。

3.1 基于触发器的方案

先明确下文所说的几个概念:

父表(parent table):基于“触发器+继承+检查约束”的分区方法中,被继承的表

子表(child table):基于“触发器+继承+检查约束”的分区方法中,继承父表的表

 

现在介绍这种方案的实现案例。

2. 首先,创建tb_event的父表:

create table tb_event

(

    event_id              varchar(64) default gen_random_uuid()  not null,

    event_type            integer,

    event_time            timestamp not null,

    event_name           varchar(128),

    person_id             varchar(64),

    person_name          varchar(64),

    dept_id               varchar(64),

    dept_name            varchar(1024),

    device_id             varchar(64),

    device_name           varchar(64),

    door_id               varchar(64),

    door_name             varchar(64),

    utc_event_time bigint not null,

    event_time_dif varchar(8) not null,

    constraint pk_tb_event primary key (event_id)

);

comment on table tb_event is '事件表';

comment on column tb_event.event_id is '主键';

comment on column tb_event.event_type is '事件类型';

comment on column tb_event.event_time is '本地事件产生时间';

comment on column tb_event.event_name is '事件名称';

comment on column tb_event.person_id is '人员ID';

comment on column tb_event.person_name is '人员姓名';

comment on column tb_event.dept_id is '部门ID';

comment on column tb_event.dept_name is '部门名称';

comment on column tb_event.device_id is '产生事件的设备id';

comment on column tb_event.device_name is '产生事件的设备名称';

comment on column tb_event.door_name is '门名称';

comment on column tb_event.door_id is '门id';

comment on column tb_event.utc_event_time is 'UTC平台事件产生的时间';

comment on column tb_event.event_time_dif is '事件产生时差';

 

2. 创建tb_event上的触发器函数和触发器:

 

create or replace function func_tri_tb_event() returns trigger as

$$

declare my_tbname varchar(64);

declare my_start_time varchar(64);

declare my_end_time varchar(64);

declare my_start_utc bigint;

declare my_end_utc bigint;

declare sql_str text;

begin

    my_tbname = TG_TABLE_NAME || '_' || to_char(to_timestamp(NEW.utc_event_time/1000),'YYYYMM');

    sql_str = 'INSERT INTO '||my_tbname ||' SELECT $1.* ';

    EXECUTE sql_str USING NEW;

    return null;

 

    exception when undefined_table then

    begin

        my_start_time=to_char(to_timestamp(NEW.utc_event_time/1000),'YYYY-MM')||'-01 00:00:00';

        my_end_time=(my_start_time::timestamp(0)  + interval '1 month')::text;

        my_start_utc=extract(epoch from my_start_time::timestamp with time zone)*1000;

        my_end_utc=extract(epoch from my_end_time::timestamp with time zone)*1000;

        execute 'create table ' ||my_tbname || ' (constraint ck_' || my_tbname || '_utctm' || ' check (utc_event_time >= ' || my_start_utc  || ' and  utc_event_time < '|| my_end_utc || ')) INHERITS ('|| TG_TABLE_NAME || ')';

        execute 'alter  table ' ||my_tbname || ' add constraint pk_'||my_tbname||' primary key (event_id)';

        execute 'create index idx_'||my_tbname ||'_utctm on ' || my_tbname || ' (utc_event_time)';

               execute 'create index idx_'||my_tbname ||'_deptid on ' || my_tbname || ' (dept_id)';

execute 'create index idx_'||my_tbname ||'_person_name on ' || my_tbname || ' (person_id)';

        EXECUTE sql_str USING NEW;

        return null;

 

        exception when others then

            EXECUTE sql_str USING NEW;

            return null;

    end;

end;

$$ language plpgsql;

 

create trigger tri_ins_tb_event BEFORE insert on tb_event for each row EXECUTE PROCEDURE func_tri_tb_event();

 

这个触发器函数做了这样一些工作:

  1. 为插入的数据创建子表,如果子表不存在。
  2. 根据utc_event_time,将数据插入到对应的子表上。

 

当我们向表中插入数据时,触发器会创建的这样的子表:

tb_event_201911,

tb_event_201920,

……

 

子表的结构如下,以 tb_event_201911 为例:

CREATE TABLE tb_event_201911

(

           event_id character varying(64) NOT NULL DEFAULT gen_random_uuid(),

            event_type integer,

            event_time timestamp without time zone NOT NULL,

           event_name character varying(128),

           person_id character varying(64),

           person_name character varying(64),

           dept_id character varying(64),

           dept_name character varying(1024),

           device_id character varying(64),

           device_name varchar(64),

           door_id character varying(64),

           door_name character varying(64),

           utc_event_time bigint NOT NULL,

           event_time_dif character varying(8) NOT NULL,

           CONSTRAINT pk_tb_event_201911 PRIMARY KEY (event_id),

           CONSTRAINT ck_tb_event_201911_utc_event_time CHECK (utc_event_time >= '1572537600000'::bigint AND utc_event_time < '1575129600000'::bigint)

)INHERITS (tb_event);

 

CREATE INDEX idx_tb_event_201911_dept_id ON tb_event_201911 (dept_id);

CREATE INDEX idx_tb_event_201911_person_name ON tb_event_201911 (person_id);

CREATE INDEX idx_tb_event_201911_utc_event_time ON tb_event_201911 (utc_event_time);

可以看出,这个子表继承(inherit)父表,并有一个check约束用来限定utc_event_time(即事件发生的utc时间)的取值范围,此外,还有独立的主键、唯一键约束和索引。这样,当程序以utc_event_time为条件在tb_event进行查询,删除或更新操作时,数据库可根据utc_event_time定位到相应的子表上。

 

这样就实现了将tb_event作为一个分区表。

 

3.2 声明式分区方案

先明确下文所说的几个概念:

分区表(partitioned table):声明式分区方法中,被划分为多个子集的表。

分区(partition):声明式分区方法中,分区表的单独子集。

 

1. 创建分区表(父表)。执行下面的sql:

create table tb_event

(

    event_id              varchar(64) default gen_random_uuid()  not null,

    event_type            integer,

    event_time            timestamp not null,

    event_name            varchar(128),

    person_id             varchar(64),

    person_name           varchar(64),

    dept_id               varchar(64),

    dept_name             varchar(1024),

    device_id             varchar(64),

    device_name           varchar(64),

    door_id               varchar(64),

    door_name             varchar(64),

    receive_time          timestamp,

    utc_event_time bigint not null,

    event_time_dif varchar(8) not null

) partition by range (utc_event_time);

comment on table tb_event is '事件表';

comment on column tb_event.event_id is '主键';

comment on column tb_event.event_type is '事件类型';

comment on column tb_event.event_time is '本地事件产生时间';

comment on column tb_event.event_name is '事件名称';

comment on column tb_event.person_id is '人员ID';

comment on column tb_event.person_name is '人员姓名';

comment on column tb_event.dept_id is '部门ID';

comment on column tb_event.dept_name is '部门名称';

comment on column tb_event.device_id is '产生事件的设备id';

comment on column tb_event.device_name is '产生事件的设备名称';

comment on column tb_event.door_name is '门名称';

comment on column tb_event.door_id is '门id';

comment on column tb_event.receive_time is '本地平台接受事件的时间';

comment on column tb_event.utc_event_time is 'UTC平台事件产生的时间';

comment on column tb_event.event_time_dif is '事件产生时差';

 

CREATE INDEX idx_tb_event_dept_id ON tb_event (dept_id);

CREATE INDEX idx_tb_event_person_name ON tb_event (person_id);

CREATE INDEX idx_tb_event_utc_event_time ON tb_event (utc_event_time);

 

注意,它没有主键或唯一键。因为在 PostgreSQL中,我们不能在分区表上创建不包含分区字段(即utc_event_time)的主键和唯一键。

 

2. 创建分区。例如,如果你要为2019年11月创建一个分区,则执行下面的sql:

create table tb_event_201909 partition of tb_event for values from (1572537600000) to (1575129600000);

 

如果希望创建一个默认分区,存放那些对应分区还没有被创建的数据。则可以执行:

create table tb_event_default partition of tb_event default;

 

 

如图,从pgadmin4 中,你可以看到创建了哪些分区:

                                                      

 

4 分区表的升级方法

我们已经介绍了这两种分区表设计方案和具体案例。那么,如何将基于触发器的分区表升级为声明式的分区表呢?下面我们将介绍升级的步骤。

 

0. 在升级之前,最好对数据库进行备份,或者至少备份要升级到的表。以便在错误发生时,可以恢复数据。

 

1. 删除父表上面的触发器和触发器函数。对于表tb_event,执行下面的命令。

drop trigger if exists tri_ins_tb_event on tb_event cascade ;

drop function if exists func_tri_tb_event() cascade;

 

2. 创建与原表结构,分区键,默认值,非空约束,描述完全相同的声明式分区表。

对于tb_event,我们创建一个 tb_event_bak,它的分区键是utc_event_time:

create table tb_event_bak

    (

        like tb_event including DEFAULTS including COMMENTS

    ) partition by range (utc_event_time);

comment on table tb_event_bak is '事件表';

 

3. 为这个分区表的创建默认的分区,为该分区加上主键,唯一键等相关约束,但不要添加检查约束。tb_event_bak的默认分区的名称是tb_event_default。操作如下:

create table tb_event_default partition of tb_event_bak default;

alter table tb_event_default add constraint pk_tb_event_default primary key (event_id);

 

4. 接下来,将原表的每一个子表变成新表的分区。

    4.1 取消子表对父表的继承。以 tb_event_201911 为例:

    alter table tb_event_201911 no inherit tb_event;

 

    4.2 根据子表上的检查约束条件,确定分区字段的取值范围,将原先的子表作为分区表的分区。

例如,根据检查约束ck_tb_event_201911_utc_event_time 可以看到, tb_event_201911的utc_event_time 的取值范围是 [1572537600000, 1575129600000), 则执行命令:

alter table tb_event_bak attach partition tb_event_201911 for values from ('1572537600000') to ('1575129600000');

 

    4.3 删除子表上原来的检查约束:

ALTER TABLE tb_event_201911 DROP CONSTRAINT ck_tb_event_201911_utc_event_time;

 

5.  对其他子表,执行上述操作,直到所有的子表都成为新表的分区。

 

6.  删除原来的父表,并将新分区表的名字改为原父表的名字。

drop table tb_event cascade;

alter table tb_event_bak rename to tb_event;

 

    4.6 可以在分区表上创建索引,这样的好处是之后创建分区时,它分区也会继承这些索引,而无需再次声明。

CREATE INDEX tb_event_dept_id_idx ON tb_event (dept_id);

CREATE INDEX tb_event_event_type_idx ON tb_event (event_type);

CREATE INDEX tb_event_person_name_idx ON tb_event (person_name); 

CREATE INDEX tb_event_utc_event_time_idx ON tb_event (utc_event_time);

 

需要注意,你不能在分区表上创建不包含分区字段的主键和唯一键。像这样的约束是无法创建的:

Alter table tb_event add constraint pk_tb_event primary key (event_id);

上面的命令会报错。因此主键,唯一键约束仍需单独为每个分区创建。

 

这样就完成了分区表的升级。

 

5 总结

本文介绍了PostgreSQL 中两种分区的方案,基于触发器的方案和声明式分区表方案及其应用案例。并根据具体案例,着重介绍了将基于触发器的方案升级为声明式分区方案的方法。由于声明式分区的性能更好,因此当程序使用了PostgreSQL 10 以上版本后,应该尽早将分区表升级为声明式分区表。

 

参考文献

[1] Ashutosh Bapat. Upgrade your partitioning from inheritance to declarative. 2018-06-23

[2] PostgreSQL全球开发小组. PostgreSQL 11.6 文档. 表分区. 2019-11-14

[3] 石勇虎.庖丁解牛之平安vacuum优化之路. PostgreSQL 2018 中国技术大会

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值