PostgreSQL——分区表
分区表
- 一、分区表的意义
- 二、传统分区表
- 2.1、继承表
- 2.2、创建分区表
- 2.3、使用分区表
- 2.4、查询父表还是子表
- 2.5、constraint_exclusion参数
- 2.6、添加分区
- 2.7、删除分区
- 2.8、分区表相关查询
- 2.9、传统分区表注意事项
- 三、内置分区表
- 3.1、创建分区表
- 3.2、使用分区表
- 3.3、内置分区表原理
- 3.4、添加分区
- 3.5、删除分区
- 3.6、更新分区数据
- 3.7、内置分区表注意事项
一、分区表的意义
分区表主要有以下优势 :
- 当查询或更新一个分区上的大部分数据时,对分区进行索引扫描代价很大,然而,在分区上使用顺序扫描能提升性能 。
- 当需要删除一个分区数据时,通过 DROP TABLE 删除一个分区,远比 DELETE 删除数据高效,特别适用于日志数据场景。
- 由于一个表只能存储在一个表空间上,使用分区表后,可以将分区放到不同的表空间上,例如可以将系统很少访问的分区放到廉价的存储设备上,也可以将系统常访问的分区存储在高速存储上。
二、传统分区表
传统分区表是通过继承和触发器方式实现的,其实现过程步骤多,非常复杂,需要定义父表、定义子表、定义子表约束、创建子表索引、创建分区插入、删除、修改函数和触发器等,可以说是在普通基础上手动实现的分区表。在介绍传统分区表之前先介绍继承,继承是传统分区表的重要组成部分。
2.1、继承表
PostgreSQL提供继承表,简单地说是首先定义一张父表,之后可以创建子表继承父表,下面通过一个简单的例子来理解。
创建一张日志模型表tbl_log:
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表结构如下:
父表和子表都可以插入数据,接着分别在父表和子表中插入一条数据,如下所示:
insert into tbl_log
values(1, '2017-08-26', null);insert into tbl_log_sql
values(2, '2017-08-27', null, 'select 2');
这时如果查询父表tbl_log会显示两表的记录,如下所示:
select * from tbl_log;
尽管查询父表会将子表的记录数也列出,但子表自定义的字段没有显示,如果想确定数据来源于哪张表,可通过以下SQL查看表的OID,如下所示:
select tableoid, *
from tbl_log;
tableoid是表的隐藏字段,表示表的OID,可以通过pg_class系统关联找到表名,如下所示:
select p.relname, c.*
from tbl_log c, pg_class p
where c.tableoid = p.oid;
如果只想查询父表的数据,需在父表名称前加上关键字ONLY,如下所示:
select *
from only tbl_log;
因此,对于UPDATE、DELETE、SELECT操作,如果父表名称前面没有加上ONLY,则会对父表和所有子表进行DML操作,如下所示:
delete from tbl_log;select count(*)
from tbl_log;
2.2、创建分区表
传统分区表创建过程主要包括以下几个步骤:
- 创建父表,如果父表上定义了约束,子表会继承,因此除非是全局约束,否则不应该在父表上定义约束,另外,父表不应该写入数据。
- 通过inherits方式创建继承表,也称之为子表或分区,子表的字段定义应该和父表保持一致。
- 给所有子表创建约束,只有满足约束条件的数据才能写入对应分区,注意分区约束值范围不要有重叠。
- 给所有子表创建索引,由于继承操作不会继承父表上的索引,因此索引需要手工创建。
- 在父表上定义insert、delete、update触发器,将SQL分发到对应分区,这步可选,因为应用可以根据分区规则定位到对应分区进行DML操作。
- 启用constraint_exclusion参数,如果这个参数设置成off,则父表上的SQL性能会降低,后面会通过示例解释这个参数。
以上六个步骤是创建传统分区表的主要步骤,接下来通过一个示例演示创建一张范围分区表,并且定义年月子表存储月数据。
首先创建父表:
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 < '2017-01-01')
) inherits(log_ins);create table log_ins_history_201701(check(create_time >= '2017-01-01' and create_time < '2017-02-01')
) inherits(log_ins);create table log_ins_history_201702(check(create_time >= '2017-02-01' and create_time < '2017-03-01')
) inherits(log_ins);create table log_ins_history_201703(check(create_time >= '2017-03-01' and create_time < '2017-04-01')
) inherits(log_ins);create table log_ins_history_201704(check(create_time >= '2017-04-01' and create_time < '2017-05-01')
) inherits(log_ins);create table log_ins_history_201705(check(create_time >= '2017-05-01' and create_time < '2017-06-01')
) inherits(log_ins);create table log_ins_history_201706(check(create_time >= '2017-06-01' and create_time < '2017-07-01')
) inherits(log_ins);create table log_ins_history_201707(check(create_time >= '2017-07-01' and create_time < '2017-08-01')
) inherits(log_ins);create table log_ins_history_201708(check(create_time >= '2017-08-01' and create_time < '2017-09-01')
) inherits(log_ins);create table log_ins_history_201709(check(create_time >= '2017-09-01' and create_time < '2017-10-01')
) inherits(log_ins);create table log_ins_history_201710(check(create_time >= '2017-10-01' and create_time < '2017-11-01')
) inherits(log_ins);create table log_ins_history_201711(check(create_time >= '2017-11-01' and create_time < '2017-12-01')
) inherits(log_ins);create table log_ins_history_201712(check(create_time >= '2017-12-01' and create_time < '2018-01-01')
) inherits(log_ins);
给子表创建索引:
create index idx_his_ctime on log_ins_history
using btree (create_time);create index idx_log_ins_201701_ctime on log_ins_history_201701
using btree (create_time);create index idx_log_ins_201702_ctime on log_ins_history_201702
using btree (create_time);create index idx_log_ins_201703_ctime on log_ins_history_201703
using btree (create_time);create index idx_log_ins_201704_ctime on log_ins_history_201704
using btree (create_time);create index idx_log_ins_201705_ctime on log_ins_history_201705
using btree (create_time);create index idx_log_ins_201706_ctime on log_ins_history_201706
using btree (create_time);create index idx_log_ins_201707_ctime on log_ins_history_201707
using btree (create_time);create index idx_log_ins_201708_ctime on log_ins_history_201708
using btree (create_time);create index idx_log_ins_201709_ctime on log_ins_history_201709
using btree (create_time);create index idx_log_ins_201710_ctime on log_ins_history_201710
using btree (create_time);create index idx_log_ins_201711_ctime on log_ins_history_201711
using btree (create_time);create index idx_log_ins_201712_ctime on log_ins_history_201712
using btree (create_time);
由于父表上不存储数据,可以不用在父表上创建索引。
创建触发器函数,设置数据插入父表时的路由规则,如下所示:
create or replace function log_ins_insert_trigger()returns triggerlanguage plpgsql
as $function$
beginif (NEW.create_time < '2017-01-01') theninsert into log_ins_history VALUES(NEW.*);elsif (NEW.create_time >= '2017-01-01' and NEW.create_time < '2017-02-01') theninsert into log_ins_history_201701 VALUES(NEW.*);elsif (NEW.create_time >= '2017-02-01' and NEW.create_time < '2017-03-01') theninsert into log_ins_history_201702 VALUES(NEW.*);elsif (NEW.create_time >= '2017-03-01' and NEW.create_time < '2017-04-01') theninsert into log_ins_history_201703 VALUES(NEW.*);elsif (NEW.create_time >= '2017-04-01' and NEW.create_time < '2017-05-01') theninsert into log_ins_history_201704 VALUES(NEW.*);elsif (NEW.create_time >= '2017-05-01' and NEW.create_time < '2017-06-01') theninsert into log_ins_history_201705 VALUES(NEW.*);elsif (NEW.create_time >= '2017-06-01' and NEW.create_time < '2017-07-01') theninsert into log_ins_history_201706 VALUES(NEW.*);elsif (NEW.create_time >= '2017-07-01' and NEW.create_time < '2017-08-01') theninsert into log_ins_history_201707 VALUES(NEW.*);elsif (NEW.create_time >= '2017-08-01' and NEW.create_time < '2017-09-01') theninsert into log_ins_history_201708 VALUES(NEW.*);elsif (NEW.create_time >= '2017-09-01' and NEW.create_time < '2017-10-01') theninsert into log_ins_history_201709 VALUES(NEW.*);elsif (NEW.create_time >= '2017-10-01' and NEW.create_time < '2017-11-01') theninsert into log_ins_history_201710 VALUES(NEW.*);elsif (NEW.create_time >= '2017-11-01' and NEW.create_time < '2017-12-01') theninsert into log_ins_history_201711 VALUES(NEW.*);elsif (NEW.create_time >= '2017-12-01' and NEW.create_time < '2018-01-01') theninsert into log_ins_history_201712 VALUES(NEW.*);else raise exception 'create_time out of range. Fix the log_ins_insert trigger() function!';end if;return null;
end;
$function$;
函数中的NEW.*是指要插入的数据行,在父表上定义插入触发器:
create trigger insert_log_ins_trigger
before insert
on log_ins
for each row
execute procedure log_ins_insert_trigger();
触发器创建完成后,向父表log_ins插入数据时,会执行触发器并触发函数log_ins_insert_trigger()将表数据插入到相应的分区中。DELETE、UPDATE触发器和函数创建过程和INSERT方式类似,传统分区表的创建步骤已全部完成。
2.3、使用分区表
向父表log_ins插入测试数据,并验证数据是否插入对应分区:
insert into log_ins(user_id, create_time)
select round(10000000*random()), generate_series('2016-12-01'::date, '2017-12-01'::date, '1 minute');
这里通过随机生成数据插入,数据如下所示:
select *
from log_ins limit 5;
查看父表数据,发现父表没有数据;
select count(*) from only log_ins;
select count(*) from log_ins;
查看子表数据:
select min(create_time), max(create_time)
from log_ins_history_201701;
这说明子表里可查到数据,查看子表大小:
由此可见数据都已经插入到子表里。
2.4、查询父表还是子表
假如我们检索2017-01-01这一天的数据,我们可以查询父表,也可以直接查询子表,两者性能上是否有差异?
查询父表的执行计划如下:
explain analyze select *
from log_ins
where create_time > '2017-01-01' and create_time < '2017-01-02';
从以上执行计划看出在分区log_ins_history_201701上进行了索引扫描,接着查看直接查询子表log_ins_history_201701的执行计划:
explain analyze select *
from log_ins_history_201701
where create_time > '2017-01-01' and create_time < '2017-01-02';
从以上执行计划看出,直接查询子表更快,如果并发量上去的话,这个差异将更明显,因此实际生产过程中,对于传统分区表分区方式,不建议应用访问父表,而是直接访问子表,也许有人会问,应用如何定位到访问哪张子表呢?可以根据预先的分区约束定义,上面的例子是根据时间范围分区,name应用可以根据时间来判断查询哪张子表,当然,以上是根据分表表分区键查询的场景,如果根据非分区键查询则会扫描分区表的所有分区。
2.5、constraint_exclusion参数
constraint_exclusion参数用来控制优化器是否根据表上的约束来优化查询,参数为以下值:
on
:所有表都通过约束优化查询off
:所有表都不通过约束优化查询partition
:只对继承表和UNION ALL子查询通过检索约束来优化查询
简单地说,如果设置成on或partition,查询父表时优化器会根据子表上的约束判断检索哪些子表,而不要扫描所有子表,从而提升查询性能。
2.6、添加分区
添加分区属于分区表维护的常规操作之一,比如历史表范围分区到期之前需要扩分区,log ins表为日志表,每个分区存储当月数据,假如分区快到期了,可通过以下SQL扩分区,首先创建子表,如下所示:
create table log_ins_history_201801(check(create_time >= '2018-01-01' and create_time < '2018-02-01')
) inherits(log_ins);
之后创建相关索引:
create index idx_log_ins_201801_ctime
on log_ins_history_201801 using btree(create_time);
然后刷新触发器函数log_ins_insert_trigger(0,添加相应代码,将符合路由规则的数据插入新分区,详见之前定义的这个函数,这步完成后,添加分区操作完成,可通过d+log_ins命令查看log_ins的所有分区。
这种方法比较直接,创建分区时就将分区继承到父表,如果中间步骤有错可能对生产系统带来影响,比较推荐的做法是将以上操作分解成以下几个步骤,降低对生产系统的影响,如下所示:
-- 创建分区
create table log_ins_history_201802(like log_ins including all
);-- 添加约束
alter table log_ins_history_201802
add constraint log_ins_history_201802_create_time_check
check (create_time >= '2018-02-01' and create_time < '2018-03-01');-- 刷新触发器函数log_ins_insert_trigger()-- 所有步骤完成后,将新分区log_ins_201802继承到父表log_ins
alter table log_ins_hisroty_201802 inherit log_ins;
以上方法是将新分区所有操作完成后,再将分区继承到父表,降低了生产系统添加分区操作的风险,当然,在生产系统添加分区前建议在测试环境事先演练一把。
2.7、删除分区
分区表的一个重要优势是对于大表的管理上十分方便,例如需要删除历史数据时可以直接删除一个分区,这比DELETE方式效率高了多个数量级,传统分区表删除分区通常有两种方法,第一种方法是直接删除分区,如下所示:
drop table log_ins_201802;
就像删除普通表一样删除分区即可,当然删除分区前需再三确认是否需要备份数据;另一种比较推荐的删除分区方法是先将分区的继承关系去掉,如下所示:
alter table log_ins_history_201802 no inherit log_ins;
执行以上命令后,log ins201802分区不再属于分区表log_ins的分区,但log_ins_history_201802表依然保留可供查询,这种方式相比方法一提供了一个缓冲时间,属于比较稳妥的删除分区方法,因为在拿掉子表继承关系后,只要没删除这个表,还可以使子表重新继承父表。
2.8、分区表相关查询
分区表创建完成后,如何查看分区表定义、分区表分区信息呢?比较常用的方法是通过\d
元命令,如下所示:
以上信息显示了表 log_ ins有 14 个分区,并且创 建了触发器 ,触发器函数为 log_ins_insert_ trigger (),如 果想列出分区名称可通过\d+ log_ins
元命令列出 。
另一种列出分区表分区信息方法是通过SQL命令:
select nmsp_parent.nspname as parent_schema,parent.relname as parent,nmsp_child.nspname as child_schema,child.relname as child_schema
from pg_inherits join pg_class parent
on pg_inherits.inhparent = parent.oid join pg_class child
on pg_inherits.inhrelid = child.oid join pg_namespace nmsp_parent
on nmsp_parent.oid = parent.relnamespace join pg_namespace nmsp_child
on nmsp_child.oid = child.relnamespace
where parent.relname = 'log_ins';
pg_inherits系统表记录了子表和父表之间的继承关系,通过以上查询列出指定分区表的分区。如果想查看一个库中有哪些分区表,并显示这些分区表的分区数量,可通过以下SQL查询:
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 in ('r', 'p')
group by 1,2
order by partition_num desc;
以上结果显示当前库中有两个分区表,log_ins分区表有14个分区,tbl_log分区表只有一个分区。
2.9、传统分区表注意事项
传统分区表的使用有以下注意事项:
- 当往父表上插入数据时,需事先在父表上创建路由函数和触发器,数据才会根据分区键路由规则插入到对应分区中,目前仅支持范围分区和列表分区。
- 分区表上的索引、约束需要使用单独的命令创建,目前没有办法一次性自动在所有分区上创建索引、约束。
- 父表和子表允许单独定义主键,因此父表和子表可能存在重复的主键记录,目前不支持在分区表上定义全局主键。
- UPDATE时不建议更新分区键数据,特别是会使数据从一个分区移动到另一分区的场景,可通过更新触发器实现,但会带来管理上的成本。
- 性能方面:根据本节的测试数据和测试场景,传统分区表根据非分区键查询相比普通表性能差距较大,因为这种场景下分区表会扫描所有分区;根据分区键查询相比普通表性能有小幅降低,而查询分区表子表性能相比普通表略有提升;
三、内置分区表
PostgreSQL10一个重量级新特性是支持内置分区表,用户不需要预先在父表上定义INSERT、DELETE、UPDATE触发器,对父表的DML操作会自动路由到相应分区,相比传统分区表大幅度降低了维护成本,目前仅支持范围分区和列表分区。
3.1、创建分区表
创建分区表的主要语法包含两部分:创建主表和创建分区。
创建主表语法如下:
create table table_name (...)
[ partition by {range | list} ({column_name | expression})]
创建主表时须指定分区方式,可选的分区方式为RANGE范围分区或LIST列表分区,并指定字段或表达式作为分区键。
创建分区的语法如下:
create table table_name() partition of parent_table for values partition_bound_spec
创建分区时必须指定是哪张表的分区,同时指定分区策略partition bound spec,如果是范围分区,partition bound spec须指定每个分区分区键的取值范围,如果是列表分区
partition_bound_spec,需指定每个分区的分区键值。
PostgreSQL10创建内置分区表主要分为以下几个步骤:
- 创建父表,指定分区键和分区策略。
- 创建分区,创建分区时须指定分区表的父表和分区键的取值范围,注意分区键的范围不要有重叠,否则会报错。
- 在分区上创建相应索引,通常情况下分区键上的索引是必须的,非分区键的索引可根据实际应用场景选择是否创建。
接下来通过创建范围分区的示例来演示内置分区表的创建过程,首先创建一张范围分区表,表名为log_par,如下所示:
create table log_par (id serial,user_id int4,create_time timestamp(0) without time zone
) partition by range(create_time);
表log par指定了分区策略为范围分区,分区键为create time字段。创建分区,并设置分区的分区键取值范围,如下所示:
create table log_par_his partition of log_par
for values from ('2016-01-01') to ('2017-01-01');create table log_par_201701 partition of log_par
for values from ('2017-01-01') to ('2017-02-01');create table log_par_201702 partition of log_par
for values from ('2017-02-01') to ('2017-03-01');create table log_par_201703 partition of log_par
for values from ('2017-03-01') to ('2017-04-01');create table log_par_201704 partition of log_par
for values from ('2017-04-01') to ('2017-05-01');create table log_par_201705 partition of log_par
for values from ('2017-05-01') to ('2017-06-01');create table log_par_201706 partition of log_par
for values from ('2017-06-01') to ('2017-07-01');create table log_par_201707 partition of log_par
for values from ('2017-07-01') to ('2017-08-01');create table log_par_201708 partition of log_par
for values from ('2017-08-01') to ('2017-09-01');create table log_par_201709 partition of log_par
for values from ('2017-09-01') to ('2017-10-01');create table log_par_201710 partition of log_par
for values from ('2017-10-01') to ('2017-11-01');create table log_par_201711 partition of log_par
for values from ('2017-11-01') to ('2017-12-01');create table log_par_201712 partition of log_par
for values from ('2017-12-01') to ('2018-01-01');
注意分区的分区键范围不要有重叠,定义分区键范围实质上给分区创建了约束。
给所有分区的分区键创建索引,如下所示:
create index idx_log_par_his_ctime
on log_par_his using btree(create_time);create index idx_log_par_201701_ctime
on log_par_201701 using btree(create_time);create index idx_log_par_201702_ctime
on log_par_201702 using btree(create_time);create index idx_log_par_201703_ctime
on log_par_201703 using btree(create_time);create index idx_log_par_201704_ctime
on log_par_201704 using btree(create_time);create index idx_log_par_201705_ctime
on log_par_201705 using btree(create_time);create index idx_log_par_201706_ctime
on log_par_201706 using btree(create_time);create index idx_log_par_201707_ctime
on log_par_201707 using btree(create_time);create index idx_log_par_201708_ctime
on log_par_201708 using btree(create_time);create index idx_log_par_201709_ctime
on log_par_201709 using btree(create_time);create index idx_log_par_201710_ctime
on log_par_201710 using btree(create_time);create index idx_log_par_201711_ctime
on log_par_201711 using btree(create_time);create index idx_log_par_201712_ctime
on log_par_201712 using btree(create_time);
以上三步完成了内置分区表的创建。
3.2、使用分区表
向分区表插入数据:
insert into log_par(user_id, create_time)
select round(100000 * random()), generate_series('2016-12-01'::date, '2017-12-01'::"date", '1 minute');
查看表数据:
select count(*)
from log_par;
select count(*)
from only log_par;
从以上结果可以看出,父表log par没有存储任何数据,数据存储在分区中,通过分区大小也可以证明这一点,如下所示:
3.3、内置分区表原理
内置分区表原理实际上和传统分区表一样,也是使用继承方式,分区可称为子表,通过以下查询很明显看出表log par和其分区是继承关系:
select nmsp_parent.nspname as parent_schema,parent.relname as parent,nmsp_child.nspname as child_schema,child.relname as child_schema
from pg_inherits join pg_class parent
on pg_inherits.inhparent = parent.oid join pg_class child
on pg_inherits.inhrelid = child.oid join pg_namespace nmsp_parent
on nmsp_parent.oid = parent.relnamespace join pg_namespace nmsp_child
on nmsp_child.oid = child.relnamespace
where parent.relname = 'log_par';
3.4、添加分区
添加分区的操作 比较简单,例如给 log_par 增加一个分区,如下所示 :
create table log_par_201801 partition of log_par
for values from ('2018-01-01') to ('2018-02-01');
之后给分区创建索引,如下所示 :
create index idx_log_par_201801_ctime
on log_par_201801 using btree(create_time);
3.5、删除分区
删除分区有两种方法,第一种方法通过 DROP 分区的方式来删除,如下所示 :
drop table log_par_201801;
D ROP 方式直接将分 区 和分 区数据删除,删除前需确 认分区数据是否需要备份,避免数据丢失;另 一种推荐的方法是解绑分区, 如下所示 :
alter table log_par detach partition log_par_201801;
绑分区只是将分区 和 父表间 的关系断开 ,分区和分区数据依然保留 ,这种方式比较稳妥,如果后续需要恢复这个分区,通过连接分区方式恢复分区即可,如下所示 :
alter table log_par attach partition log_par_201801 for values from ('2018-01-01') to ('2018-02-01');
连接分区时需要指定分区上的约束 。
3.6、更新分区数据
内置分区 表 UPDAT E 操作目前不支持新记录跨分区的情况, 也就是说只允许分区 内的更新 , 例如以下 SQL 会报错:
update log_par set create_time = '2017-02-02 01:01:01'
where user_id = 16965492;
以上 user_id 等于 16965492 的记录位于 log_par_201701 分区,将这条记录的 create_time 更新为 ’ 2017-02-02 01 : 01:01 ’由于违反了当前分区的约束将报错,如果更新的数据不违反当前分区的约束则可正常更新数据,如下所示:
update log_par set create_time = '2017-01-01 01:01:01'
where user_id = 16965492;
目前内置分区表的这一 限制对于日志表影响不大,对于业务表有一定影响,使用时需注意 。
3.7、内置分区表注意事项
- 当往父表上插入数据时,数据会自动根据分区键路由规则插入到分区中,目前仅支持范围分区和列表分区。
- 分区表上的索引、约束需使用单独的命令创建,目前没有办法一次性自动在所有分区上创建索引、约束。
- 内置分区表不支持定义(全局)主键,在分区表的分区上创建主键是可以的。
- 内置分区表的内部实现使用了继承。
- 如果UPDATE语句的新记录违反当前分区键的约束则会报错,UPDAET语句的新记录目前不支持跨分区的情况。
- 性能方面:根据本节的测试场景,内置分区表根据非分区键查询相比普通表性能差距较大,因为这种场景分区表的执行计划会扫描所有分区;根据分区键查询相比普通表性能有小幅降低,而查询分区表子表性能相比普通表略有提升。