存储数据量很大时如果用单表储存数据,查询时间将会变得很长,因此我们使用分区表来提高查询效率。并且使用分区表后删除数据时,ALTER TABLE DETACH PARTITION或使用DROP TABLE删除单个分区比批量操作要快得多。这些命令还完全避免了由批量DELETE引起的VACUUM开销。
PostgreSQL 10.x 之前的版本提供了一种“手动”方式使用分区表的方式,需要使用继承 + 触发器的来实现分区表,步骤较为繁琐,需要定义附表、子表、子表的约束、创建子表索引,创建分区删除、修改,触发器等。
PostgreSQL 10.x 开始提供了内置分区表(内置是相对于 10.x 之前的手动方式)。内置分区简化了操作,将部分操作内置,最终简单三步就能够创建分区表。但是只支持范围分区(RANGE)和列表分区(LIST),11.x 版本添加了对 HASH 分区。
以下示例来自pg官方文档
官方文档-中文版
官方文档-英文版
CREATE TABLE measurement (
city_id int not null,
logdate date not null,
peaktemp int,
unitsales int
) PARTITION BY RANGE (logdate);
注意,如果表定义了主键,那么分区键(这里range中的logdate)需要是主键或主键之一。
CREATE TABLE measurement_y2006m02 PARTITION OF measurement
FOR VALUES FROM ('2006-02-01') TO ('2006-03-01');
CREATE TABLE measurement_y2006m03 PARTITION OF measurement
FOR VALUES FROM ('2006-03-01') TO ('2006-04-01');
如果使用LIST方式而非range,则1、2步可以像这样:
CREATE TABLE code_user (
user_id bigserial NOT NULL,
username varchar(255) NOT NULL,
user_phone varchar(255) NOT NULL,
user_password varchar(255) NOT NULL,
report_status bool NOT NULL
)PARTITION BY list (report_status);
CREATE TABLE "code_user_1" PARTITION OF code_user FOR VALUES in (true);
CREATE TABLE "code_user_2" PARTITION OF code_user FOR VALUES in (false);
在时间戳作为分区键时,注意10位时间戳和13位不能混用,不然就会没法插入表(毕竟是Long类型),如果有以下分区表:
CREATE TABLE public.task_minute_v2 (
i_id bigserial NOT NULL,
c_camera_id varchar(140) NULL,
……
i_statistic_time int8 NOT NULL,
CONSTRAINT task_minute_v2_pkey PRIMARY KEY (i_id,i_statistic_time)
)PARTITION BY range (i_statistic_time);
CREATE TABLE task_minute_v2_20221209 PARTITION OF task_minute_v2
FOR VALUES FROM (1670515200) TO (1670601600);
CREATE TABLE task_minute_v2_20221210 PARTITION OF task_minute_v2
FOR VALUES FROM (1670601600) TO (1670688000);
当需要插入的记录i_statistic_time值为1670680000时可以插入成功,但如果为1670682000000这种形式,数据库就会报错找不到合适的分区表。
如果分区表还需要进行分区
CREATE TABLE measurement_y2006m02 PARTITION OF measurement
FOR VALUES FROM ('2006-02-01') TO ('2006-03-01')
PARTITION BY RANGE (peaktemp);
在分区键上创建索引。
CREATE INDEX ON measurement (logdate);
这里的索引并不是必须建的,但是在大多数情况建它都是有好处的。
同时需要确保enable_partition_pruning配置参数在postgresql.conf中没有被禁用,如果被禁用就无法优化查询。
将分区表从主表中移除:
ALTER TABLE measurement DETACH PARTITION measurement_y2006m02;
这时候只是解除了和主表的联系,在磁盘中子表还独立存在。
同样,也可以使用ATTACH将独立的表关联到主表上:
ALTER TABLE measurement_y2008m02 ADD CONSTRAINT y2008m02
CHECK ( logdate >= DATE '2008-02-01' AND logdate < DATE '2008-03-01' );
ALTER TABLE measurement ATTACH PARTITION measurement_y2008m02
FOR VALUES FROM ('2008-02-01') TO ('2008-03-01' );
还可以直接删除子表:
DROP TABLE measurement_y2006m02;
需要注意的是,向分区表中增加分区或者从分区表移除分区,或直接删除分区表,都需要获取父表的ACCESS EXCLUSIVE锁。推荐先移除再DROP比较好,这样的话DROP前还可以做一些诸如备份之类的操作。(是不是移除的时间也会比删除的时间更短一点,没找到资料,只是猜测)
测试了spring data jpa和postgres分区表对接,jpa只要操作主表就可以,和操作整表并不会有什么区别。需要注意的是,如果使用jpa自动建表,建的是非分区表,暂时我并没有找到可以加入partition by语句直接建分区表的方法,暂时只能使用spring.datasource.data规定sql去在jpa自动建表前先把表建起来(一个非常不优雅的方式,而且对springboot版本还有要求,在早期版本时spring.datasource.data是晚于jpa自动建表执行的)。然后再每天定时在代码中新建所需的分区表和移除不需要的分区表即可。具体可以参考postgresql把日期作为表名的一部分动态建立分区表.
2022.12.14更新
关于建表 找到了一种更为合适的方法,那就是先由jpa自动建表,再通过以下语句去把非分区表转换为分区表:
-- 新建一个table用于测试
CREATE TABLE public.my_test_table2 (
user_id bigserial NOT NULL,
username varchar(255) NOT NULL,
report_status bool NOT NULL
);
--重命名原来的非分区表
ALTER TABLE my_test_table2 RENAME TO my_test_table2_raw;
--建新的主表
CREATE TABLE my_test_table2 ( LIKE my_test_table2_raw INCLUDING ALL ) PARTITION BY list(username);
--根据新建的主表分区,可以成功
CREATE TABLE "my_test_table2_p1" PARTITION OF my_test_table2 FOR VALUES in ('yogi');
但是注意,这样有一个问题,就是当想要删除原来的表格my_test_table2_raw时可能会删除失败,例如我这里的user_id是自增的,就会有问题,drop时报错如下:
SQL 错误 [2BP01]: 错误: 无法删除 表 my_test_table2_raw 因为有其它对象倚赖它
Detail: 表 my_test_table2的列user_id的缺省值 倚赖于 序列 my_test_table2_user_id_seq
表 my_test_table2_p1的列user_id的缺省值 倚赖于 序列 my_test_table2_user_id_seq
Hint: 使用 DROP … CASCADE 把倚赖对象一并删除.
在这里显然我不能一并删除,在表的量不大的情况下,可能需要暂时保留my_test_table2_raw的表了。