PostgreSQL 10 - 数据分区

一个块的默认大小是8KB,所以,PostgreSQL的一张表可以保存最多32TB的数据。但是,大表太不方便了,应该使用分区表。
从PostgreSQL 10.0开始,用户可以更方便地处理数据分区了。

增加分区

我们先看看旧的分区办法。
在研究分区的优点之前,先展示一下如何创建分区。一切从父表开始:

postgres=# CREATE TABLE t_data (id serial, t date, payload text);
CREATE TABLE

表一共3列,我们会使用日期列做分区。
有了父表,就可以创建子表:

postgres=# CREATE TABLE t_data_2016 () INHERITS (t_data);
CREATE TABLE
postgres=# \d t_data_2016
                          数据表 "public.t_data_2016"
  栏位   |  类型   | Collation | Nullable |              Default               
---------+---------+-----------+----------+------------------------------------
 id      | integer |           | not null | nextval('t_data_id_seq'::regclass)
 t       | date    |           |          | 
 payload | text    |           |          | 
继承: t_data

t_data_2016继承了t_data。()的意思是表里没有额外的列。继承的意思是父的所有列在子表内都有效。而且,id列会继承父表的sequence,所以所有的子都分享相同的数字。
增加更多的表:

postgres=# CREATE TABLE t_data_2015 () INHERITS (t_data);
CREATE TABLE
postgres=# CREATE TABLE t_data_2014 () INHERITS (t_data);
CREATE TABLE

这些表都一样,继承自t_data。当然,子表也可以有父表不包含的列:

postgres=# CREATE TABLE t_data_2013 (special text) INHERITS (t_data);
CREATE TABLE

这样,很轻松地加了一列。它对父表没影响,只是丰富了子表-可以保存更多信息。
创建了这么多表,我们插入一行:

postgres=# INSERT INTO t_data_2015 (t, payload)
postgres-# VALUES ('2015-05-04', 'some data');
INSERT 0 1

重要的是,父表也可以看到所有子表的数据:

postgres=# SELECT * FROM t_data;
 id |     t      |  payload  
----+------------+-----------
  1 | 2015-05-04 | some data
(1 行记录)

要想理解PostgreSQL如何处理分区,可以看看执行计划:

postgres=# EXPLAIN SELECT * FROM t_data;
                              QUERY PLAN                              
----------------------------------------------------------------------
 Append  (cost=0.00..106.16 rows=4411 width=40)
   ->  Seq Scan on t_data  (cost=0.00..0.00 rows=1 width=40)
   ->  Seq Scan on t_data_2016  (cost=0.00..22.00 rows=1200 width=40)
   ->  Seq Scan on t_data_2015  (cost=0.00..22.00 rows=1200 width=40)
   ->  Seq Scan on t_data_2014  (cost=0.00..22.00 rows=1200 width=40)
   ->  Seq Scan on t_data_2013  (cost=0.00..18.10 rows=810 width=40)
(6 行记录)

实际上,过程很简单。PostgreSQL把父表以及全部子表的数据都统一到一起。这些表都是单独的,只是逻辑地连到一起。

使用表约束

如果有过滤,会怎么处理?

postgres=# EXPLAIN SELECT * FROM t_data WHERE t = '2016-01-04';
                            QUERY PLAN                             
-------------------------------------------------------------------
 Append  (cost=0.00..95.24 rows=23 width=40)
   ->  Seq Scan on t_data  (cost=0.00..0.00 rows=1 width=40)
         Filter: (t = '2016-01-04'::date)
   ->  Seq Scan on t_data_2016  (cost=0.00..25.00 rows=6 width=40)
         Filter: (t = '2016-01-04'::date)
   ->  Seq Scan on t_data_2015  (cost=0.00..25.00 rows=6 width=40)
         Filter: (t = '2016-01-04'::date)
   ->  Seq Scan on t_data_2014  (cost=0.00..25.00 rows=6 width=40)
         Filter: (t = '2016-01-04'::date)
   ->  Seq Scan on t_data_2013  (cost=0.00..20.12 rows=4 width=40)
         Filter: (t = '2016-01-04'::date)
(11 行记录)

PostgreSQL会在所有分区上应用该过滤。它不知道表名和表的内容是相关的。现在的问题是:如何教数据库知道2016表只包含2016年的数据,而2015表只包含2015年的数据呢?表约束就是这样做的。他们告诉PostgreSQL那些表的内容,因此允许优化器作出比以前更聪明的决定。这叫约束排除,在很多时候,有助于加速查询速度。
如何增加表约束呢?

postgres=# ALTER TABLE t_data_2013 ADD CHECK (t < '2014-01-01');
ALTER TABLE
postgres=# ALTER TABLE t_data_2014 ADD CHECK (t >= '2014-01-01' AND t < '2015-01-01');
ALTER TABLE
postgres=# ALTER TABLE t_data_2015 ADD CHECK (t >= '2015-01-01' AND t < '2016-01-01');
ALTER TABLE
postgres=# ALTER TABLE t_data_2016 ADD CHECK (t >= '2016-01-01' AND t < '2017-01-01');
ALTER TABLE

这样,每张表都增加了CHECK约束。
只有在表内的数据都正确-每一行都满足约束的情况下,PostgreSQL才会成功地增加约束。
咱们看看增加约束以后,发生了什么?

postgres=# EXPLAIN SELECT * FROM t_data WHERE t = '2016-01-04';
                            QUERY PLAN                             
-------------------------------------------------------------------
 Append  (cost=0.00..25.04 rows=7 width=40)
   ->  Seq Scan on t_data  (cost=0.00..0.00 rows=1 width=40)
         Filter: (t = '2016-01-04'::date)
   ->  Seq Scan on t_data_2016  (cost=0.00..25.00 rows=6 width=40)
         Filter: (t = '2016-01-04'::date)
(5 行记录)

优化器能删除很多表,只留下可能包含数据的表。

修改继承结构

偶尔需要使用ALTER TABLE修改数据结构。问题是:分区的表如何修改呢?基本上,可以在父表上增加或者删除列。PostgreSQL会自动传播这些改变,子表就跟着修改了:

postgres=# ALTER TABLE t_data ADD COLUMN x int;
ALTER TABLE
postgres=# \d t_data_2016;
                          数据表 "public.t_data_2016"
  栏位   |  类型   | Collation | Nullable |              Default               
---------+---------+-----------+----------+------------------------------------
 id      | integer |           | not null | nextval('t_data_id_seq'::regclass)
 t       | date    |           |          | 
 payload | text    |           |          | 
 x       | integer |           |          | 
检查约束限制
    "t_data_2016_t_check" CHECK (t >= '2016-01-01'::date AND t < '2017-01-01'::date)
继承: t_data

需要注意的是,列可以这样修改,但是索引不行。在一个继承的结构里,每个表有单独的索引。如果你给父表加一个索引,它只加到父表-而不会部署到子表。

把表移入或者移出分区结构

假设你有一个继承结构。数据根据i日期分区,你想提供最近几年的数据。同时,还想删除一些陈旧的用户不接触的数据。
PostgreSQL提供了一个简单的办法。首先,增加新的父表:

postgres=# CREATE TABLE t_history (LIKE t_data);
CREATE TABLE

使用LIKE,你新增加的表和t_data表的结构完全相同-包括索引、约束和默认值。
然后,可以把旧的子表移动到新的父表下面:

postgres=# ALTER TABLE t_data_2013 NO INHERIT t_data;
ALTER TABLE
postgres=# ALTER TABLE t_data_2013 INHERIT t_history;
ALTER TABLE

当然,整个过程也可以放进一个事务内,变成原子的操作。

清除数据

分区表的一个优点是能够快速清理数据。假设我们想删除一整年的数据,可以直接DROP TABLE:

postgres=# DROP TABLE t_data_2014;
DROP TABLE

你看到了,可以很容易地删除子表。但是对于父表呢?

postgres=# DROP TABLE t_data;
错误:  无法删除 表 t_data 因为有其它对象倚赖它
描述:  default value for column id of 表 t_data_2013 倚赖于 序列 t_data_id_seq
表 t_data_2016 倚赖于 表 t_data
表 t_data_2015 倚赖于 表 t_data
提示:  使用 DROP .. CASCADE 把倚赖对象一并删除.

DROP TABLE会警告我们,有依赖它的对象,拒绝drop那些表。CASCADE可以强制PostgreSQL删除那些对象:

postgres=# DROP TABLE t_data CASCADE;
注意:  串联删除3个其它对象
描述:  递归删除 default value for column id of 表 t_data_2013
递归删除 表 t_data_2016
递归删除 表 t_data_2015
DROP TABLE

理解PostgreSQL 10.0的分区

多年来,PostgreSQL致力于内置分区,PostgreSQL 10.0终于发布了第一个实现。10.0的分区功能还很基础,不过,很多未来改进的设施已经就位。
我们看一个范围分区的简单例子:

postgres=# CREATE TABLE data (             
payload integer, id integer
) PARTITION BY RANGE (payload);
CREATE TABLE
postgres=# CREATE TABLE positives PARTITION
OF data FOR VALUES FROM (0) TO (MAXVALUE);
CREATE TABLE
postgres=# CREATE TABLE negatives PARTITION
OF data FOR VALUES FROM (MINVALUE) TO (0);
CREATE TABLE

一个分区保存负数值,另一个分区保存正数值。创建父表的时候,可以简单地指定分区办法。
PostgreSQL 10.0,只支持范围分区和列表分区。11.0会支持hash分区。一旦创建了父表,就可以创建分区了-为此增加了PARTITION OF子句。


版权声明:本文为weixin_43364172原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。