PostgreSQL分区表(partitioning)应用实例详解

    前言

    项目中有需求要垂直分表,即按照时间区间将数据拆分到n个表中,PostgreSQL提供了分区表的功能。分区表实际上是把逻辑上的一个大表分割成物理上的几小块,提供了很多好处,比如:

    1、查询性能大幅提升
    2、删除历史数据更快
    3、可将不常用的历史数据使用表空间技术转移到低成本的存储介质上
    那么什么时候该使用分区表呢?官方给出的指导意见是:当表的大小超过了数据库服务器的物理内存大小则应当使用分区表,接下来结合一个例子具体记录一下创建分区表的详细过程。

    创建分区表

    首先看一下需求,现在有一张日志表,现在需要按表中的操作时间字段(operation_time)分区,如下图:

    这个需求就是一个典型的按时间创建分区表,首先看一下步骤:

    1.创建父表
    2.创建n个子表,每个子表都是继承于父表
    3.定义一个规则(Rule)或触发器(Trigger),把对主表的数据插入重定向到合适的分区表

    如上所示,整体的大步骤就分为以上三个,当然还可以有一些小的优化措施,比如对于每个分区,在关键字字段上创建一个索引等等。首先来看第一步——创建父表。

    在创建分区表之前应当先创建一张“父表”,所有分区表都从它继承,这个表中没有数据,也不要在这个表上定义任何检查约束及索引,现在我们就先创建这样一张表,但之前先建一个序列:

    
    CREATE SEQUENCE "public"."t_sys_log_main_id_seq"
     INCREMENT 1
     MINVALUE 1
     MAXVALUE 99999999
     START 1
     CACHE 1;
    
    ALTER TABLE "public"."t_sys_log_main_id_seq" OWNER TO "postgres";
    
    

    接下来创建“父表”,因为是日志表,所以表名命名为“t_sys_log_main”:

    
    CREATE TABLE "public"."t_sys_log_main" (
    "id" int4 DEFAULT nextval('t_sys_log_main_id_seq'::regclass) NOT NULL,
    "account_affiliation_code" varchar(100) COLLATE "default" NOT NULL,
    "account_affiliation" varchar(50) COLLATE "default" NOT NULL,
    "operation_time" timestamp(6) NOT NULL,
    "operation_key" varchar(2) COLLATE "default" NOT NULL,
    "operation_value" varchar(30) COLLATE "default" NOT NULL,
    "operation_loginid" varchar(100) COLLATE "default" NOT NULL,
    "operation_message" varchar(300) COLLATE "default" NOT NULL,
    "operation_ip" varchar(30) COLLATE "default" NOT NULL
    )
    WITH (OIDS=FALSE)
    
    ;
    COMMENT ON TABLE "public"."t_sys_log_main" IS '系统日志表';
    COMMENT ON COLUMN "public"."t_sys_log_main"."account_affiliation_code" IS '帐号所属机构代码';
    COMMENT ON COLUMN "public"."t_sys_log_main"."account_affiliation" IS '帐号所属机构';
    COMMENT ON COLUMN "public"."t_sys_log_main"."operation_time" IS '操作时间';
    COMMENT ON COLUMN "public"."t_sys_log_main"."operation_key" IS '操作类型(key)';
    COMMENT ON COLUMN "public"."t_sys_log_main"."operation_value" IS '操作类型(value)';
    COMMENT ON COLUMN "public"."t_sys_log_main"."operation_loginid" IS '操作帐号';
    COMMENT ON COLUMN "public"."t_sys_log_main"."operation_message" IS '操作信息';
    COMMENT ON COLUMN "public"."t_sys_log_main"."operation_ip" IS '登录地址';
    
    ALTER TABLE "public"."t_sys_log_main" ADD PRIMARY KEY ("id");
    
    

    运行以上DDL语句创建父表,创建成功后接下来就可以挨个创建分区表了,由于每个分区表都是从父表继承的,所以分区表不会增加任何字段,下面我们按需求创建4张分区子表,分别用于存放9月、10月、11月和12月的日志数据:

    
    create table t_sys_log_y2016m09
    (CHECK (operation_time >= DATE '2016-09-01' AND operation_time< DATE '2016-10-01'))
    INHERITS (t_sys_log_main);
    
    create table t_sys_log_y2016m10
    (CHECK (operation_time >= DATE '2016-10-01' AND operation_time< DATE '2016-11-01'))
    INHERITS (t_sys_log_main);
    
    create table t_sys_log_y2016m11
    (CHECK (operation_time >= DATE '2016-11-01' AND operation_time< DATE '2016-12-01'))
    INHERITS (t_sys_log_main);
    
    create table t_sys_log_y2016m12
    (CHECK (operation_time >= DATE '2016-12-01' AND operation_time< DATE '2017-01-01'))
    INHERITS (t_sys_log_main);
    
    

    如上所示,运行完成后即可创建4张分区子表,在上面的语句中我们添加了一个约束表示只允许插入本月的数据,接下来在这4张分区表的每个分区键上建立索引:

    
    create index t_sys_log_y2016m09_operation_time ON t_sys_log_y2016m09(operation_time);
    create index t_sys_log_y2016m10_operation_time ON t_sys_log_y2016m10(operation_time);
    create index t_sys_log_y2016m11_operation_time ON t_sys_log_y2016m11(operation_time);
    create index t_sys_log_y2016m12_operation_time ON t_sys_log_y2016m12(operation_time);
    
    

    到此为止我们的分区表就创建完毕了,接下来需要考虑数据插入的问题,如何才能让不同日期的数据自动的插入与其对应的分区子表中呢?有两种解决方案,分别是:规则(Rule)和触发器(Trigger),相比触发器,Rule的开销更大,所以我在这里就不做过多介绍了,下面直接介绍Trigger的方式。

    Trigger通常会结合自定义函数(Function)来实现分区插入,Function负责根据条件选择插入,而Trigger则负责Function的自动调用。首先定义Function,功能很简单,即根据日期区间insert数据即可:

    
    CREATE
    OR REPLACE FUNCTION sys_log_insert_trigger () RETURNS TRIGGER AS $$
    BEGIN
    
    IF (
      NEW .operation_time >= DATE '2016-09-01'
      AND NEW .operation_time < DATE '2016-10-01'
    ) THEN
      INSERT INTO t_sys_log_y2016m09
    VALUES
      (NEW .*) ;
    ELSEIF (
      NEW .operation_time >= DATE '2016-10-01'
      AND NEW .operation_time < DATE '2016-11-01'
    ) THEN
      INSERT INTO t_sys_log_y2016m10
    VALUES
      (NEW .*) ;
    ELSEIF (
      NEW .operation_time >= DATE '2016-11-01'
      AND NEW .operation_time < DATE '2016-12-01'
    ) THEN
      INSERT INTO t_sys_log_y2016m11
    VALUES
      (NEW .*) ;
    ELSEIF (
      NEW .operation_time >= DATE '2016-12-01'
      AND NEW .operation_time < DATE '2017-01-01'
    ) THEN
      INSERT INTO t_sys_log_y2016m12
    VALUES
      (NEW .*) ;
    ELSE
      RAISE EXCEPTION 'Date out of range!' ;
    END
    IF ; RETURN NULL ;
    END ; $$ LANGUAGE plpgsql;
    
    

    最后再创建触发器用于执行刚才的Function:

    
    CREATE TRIGGER sys_log_insert_trigger BEFORE INSERT ON t_sys_log_main
    FOR EACH ROW
    EXECUTE PROCEDURE sys_log_insert_trigger();
    
    

    到这里就全部创建完成了,最后测试一下看看结果。为了确认我们的触发器的确触发了,我们打开存储过程的统计开关,在postgresql.conf中,找到track_functions,改成all:

    接下来就可以运行几条测试insert语句来看看是否能把指定的时间记录分别插入到与其对应的分区子表中,插入之前先看下sys_log_insert_trigger()的统计信息:

    可以看到目前没有统计记录,接下来插入几条测试数据:

    
    INSERT INTO t_sys_log_main VALUES 
    (1,'200022', '西安高新第一中学初中校区', '2016-9-8 18:49:26.004', '01', 'xx', 'zhsz_t', 'test!', '127.0.0.1');
    INSERT INTO t_sys_log_main VALUES 
    (1,'200023', '西安高新第一中学初中校区', '2016-9-12 18:49:26.004', '01', 'xx', 'zhsz_t', 'test!', '127.0.0.1');
    INSERT INTO t_sys_log_main VALUES 
    (1,'200024', '西安高新第一中学初中校区', '2016-10-8 18:49:26.004', '01', 'xx', 'zhsz_t', 'test!', '127.0.0.1');
    INSERT INTO t_sys_log_main VALUES 
    (1,'200025', '西安高新第一中学初中校区', '2016-11-8 18:49:26.004', '01', 'xx', 'zhsz_t', 'test!', '127.0.0.1');
    INSERT INTO t_sys_log_main VALUES 
    (1,'200026', '西安高新第一中学初中校区', '2016-12-8 18:49:26.004', '01', 'xx', 'zhsz_t', 'test!', '127.0.0.1');
    INSERT INTO t_sys_log_main VALUES 
    (1,'200027', '西安高新第一中学初中校区', '2016-12-25 18:49:26.004', '01', 'xx', 'zhsz_t', 'test!', '127.0.0.1');
    
    

    紧接着再看一下sys_log_insert_trigger()的统计信息:

    如上图,可以看出调用了6次函数,因为我们插入了6条数据,至此分区表由创建到测试的整个过程就已经成功完成了。

    总结

    简单记录一下PostgreSQL创建分区表的完整步骤以及注意事项,希望对遇到同样问题的朋友有所帮助,The End。