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子句。