create table sales (id int, date date, amt decimal(10,2))
distributed by (id)
partition by range (date)
( start (date '2017-01-01') inclusive
end (date '2017-02-01') exclusive
every (interval '1 day') );
上面的语句以date列作为分区键,从2017年1月1月到2017年2月1日,每天一个分区,将建立31个分区。分区对应表对象的名称分别是sales_1_prt_1 ... sales_1_prt_31。注意inclusive表示分区中包含定义的分区键值,exclusive表示不包含。例如,sales_1_prt_1包含date >= (date '2017-01-01') and date < (date '2017-01-02')的数据,sales_1_prt_31包含date >= (date '2017-01-31') and date < (date '2017-02-01')的数据,即这个语句定义的分区是左闭右开的数据区间。
db1=# insert into sales values (1, (date '2016-12-31'),100);
ERROR: no partition for partitioning key (seg21 hdp4:40000 pid=60186)
db1=# insert into sales values (1, (date '2017-01-01'),100);
INSERT 0 1
db1=# insert into sales values (1, (date '2017-02-01'),100);
ERROR: no partition for partitioning key (seg23 hdp4:40000 pid=60190)
db1=# insert into sales values (1, (date '2017-01-31'),100);
INSERT 0 1
同样可以定义左开右闭的分区。
create table sales (id int, date date, amt decimal(10,2))
distributed by (id)
partition by range (date)
( start (date '2017-01-01') exclusive
end (date '2017-02-01') inclusive
every (interval '1 day') );
db1=# insert into sales values (1, (date '2017-01-01'),100);
ERROR: no partition for partitioning key (seg19 hdp4:40000 pid=60182)
db1=# insert into sales values (1, (date '2017-01-02'),100);
INSERT 0 1
db1=# insert into sales values (1, (date '2017-01-31'),100);
INSERT 0 1
db1=# insert into sales values (1, (date '2017-02-01'),100);
INSERT 0 1
db1=# insert into sales values (1, (date '2017-02-02'),100);
ERROR: no partition for partitioning key (seg23 hdp4:40000 pid=60269)
也可以显式定义每个分区。
create table sales (id int, date date, amt decimal(10,2))
distributed by (id)
partition by range (date)
( partition p201701 start (date '2017-01-01') inclusive ,
partition p201702 start (date '2017-02-01') inclusive ,
partition p201703 start (date '2017-03-01') inclusive ,
partition p201704 start (date '2017-04-01') inclusive ,
partition p201705 start (date '2017-05-01') inclusive ,
partition p201706 start (date '2017-06-01') inclusive ,
partition p201707 start (date '2017-07-01') inclusive ,
partition p201708 start (date '2017-08-01') inclusive ,
partition p201709 start (date '2017-09-01') inclusive ,
partition p201710 start (date '2017-10-01') inclusive ,
partition p201711 start (date '2017-11-01') inclusive ,
partition p201712 start (date '2017-12-01') inclusive
end (date '2018-01-01') exclusive );
以上语句为2017年每个月建立一个分区。注意,不需要问每个分区指定END值,只要在最后一个分区(本例中的p201712)指定END值即可。
db1=# create table rank (id int, rank int, year int, gender
db1(# char(1), count int)
db1-# distributed by (id)
db1-# partition by range (year)
db1-# ( start (2017) end (2018) every (1),
db1(# default partition extra );
NOTICE: CREATE TABLE will create partition "rank_1_prt_extra" for table "rank"
NOTICE: CREATE TABLE will create partition "rank_1_prt_2" for table "rank"
CREATE TABLE
db1=# \dt
List of relations
Schema | Name | Type | Owner | Storage
--------+------------------+-------+---------+-------------
public | rank | table | gpadmin | append only
public | rank_1_prt_2 | table | gpadmin | append only
public | rank_1_prt_extra | table | gpadmin | append only
(3 rows)
db1=# insert into rank values (1,1,2016,'M',100);
INSERT 0 1
db1=# insert into rank values (1,1,2017,'M',100);
INSERT 0 1
db1=# insert into rank values (1,1,2018,'M',100);
INSERT 0 1
db1=# insert into rank values (1,1,2019,'M',100);
INSERT 0 1
db1=# select * from rank;
id | rank | year | gender | count
----+------+------+--------+-------
1 | 1 | 2016 | M | 100
1 | 1 | 2018 | M | 100
1 | 1 | 2019 | M | 100
1 | 1 | 2017 | M | 100
(4 rows)
db1=# select * from rank_1_prt_2;
id | rank | year | gender | count
----+------+------+--------+-------
1 | 1 | 2017 | M | 100
(1 row)
db1=# select * from rank_1_prt_extra;
id | rank | year | gender | count
----+------+------+--------+-------
1 | 1 | 2016 | M | 100
1 | 1 | 2018 | M | 100
1 | 1 | 2019 | M | 100
(3 rows)
db1=# drop table rank;
DROP TABLE
db1=# \dt
No relations found.
从上面的例子看到:
db1=# create table rank (id int, rank int, year int, gender
db1(# char(1), count int )
db1-# distributed by (id)
db1-# partition by list (gender)
db1-# ( partition girls values ('f'),
db1(# partition boys values ('m'),
db1(# default partition other );
NOTICE: CREATE TABLE will create partition "rank_1_prt_girls" for table "rank"
NOTICE: CREATE TABLE will create partition "rank_1_prt_boys" for table "rank"
NOTICE: CREATE TABLE will create partition "rank_1_prt_other" for table "rank"
CREATE TABLE
db1=# \dt
List of relations
Schema | Name | Type | Owner | Storage
--------+------------------+-------+---------+-------------
public | rank | table | gpadmin | append only
public | rank_1_prt_boys | table | gpadmin | append only
public | rank_1_prt_girls | table | gpadmin | append only
public | rank_1_prt_other | table | gpadmin | append only
(4 rows)
db1=# insert into rank values (1,1,2016,'M',100);
INSERT 0 1
db1=# insert into rank values (1,1,2016,'m',100);
INSERT 0 1
db1=# insert into rank values (1,1,2016,'f',100);
INSERT 0 1
db1=# insert into rank values (1,1,2016,'F',100);
INSERT 0 1
db1=# insert into rank values (1,1,2016,'A',100);
INSERT 0 1
db1=# select * from rank;
id | rank | year | gender | count
----+------+------+--------+-------
1 | 1 | 2016 | f | 100
1 | 1 | 2016 | m | 100
1 | 1 | 2016 | M | 100
1 | 1 | 2016 | F | 100
1 | 1 | 2016 | A | 100
(5 rows)
db1=# select * from rank_1_prt_boys;
id | rank | year | gender | count
----+------+------+--------+-------
1 | 1 | 2016 | m | 100
(1 row)
db1=# select * from rank_1_prt_girls;
id | rank | year | gender | count
----+------+------+--------+-------
1 | 1 | 2016 | f | 100
(1 row)
db1=# select * from rank_1_prt_other;
id | rank | year | gender | count
----+------+------+--------+-------
1 | 1 | 2016 | M | 100
1 | 1 | 2016 | F | 100
1 | 1 | 2016 | A | 100
(3 rows)
HAWQ不支持多分区键列复合比较,分区键只能是单列。
db1=# create table rank (id int, rank int, year int, gender
db1(# char(1), count int )
db1-# distributed by (id)
db1-# partition by list (gender,year)
db1-# ( partition girls values ('f',2017),
db1(# partition boys values ('m',2018),
db1(# default partition other );
ERROR: Composite partition keys are not allowed
create table sales (trans_id int, date date, amount
decimal(9,2), region text)
distributed by (trans_id)
partition by range (date)
subpartition by list (region)
subpartition template
( subpartition usa values ('usa'),
subpartition asia values ('asia'),
subpartition europe values ('europe'),
default subpartition other_regions)
(start (date '2017-01-01') inclusive
end (date '2018-01-01') exclusive
every (interval '1 month'),
default partition outlying_dates );
以上语句建立了一共65个分区。一级分区13个,每个一级分区包含4个子分区。
create table sales (id int, year int, month int, day int,
region text)
distributed by (id)
partition by range (year)
subpartition by range (month)
subpartition template (
start (1) end (13) every (1),
default subpartition other_months )
subpartition by list (region)
subpartition template (
subpartition usa values ('usa'),
subpartition europe values ('europe'),
subpartition asia values ('asia'),
default subpartition other_regions )
( start (2017) end (2018) every (1),
default partition outlying_years );
注意,范围分区上的多级分区很容易建立大量的分区,其中有些分区可能只有很少的数据(甚至没有数据)。随着分区数量的增加,系统表的记录不断增长,查询优化和执行时所需的内存也会增加。加大范围分区的范围或者选择不同的分区策略有助于减少分区的数量。
create table sales2 (like sales)
partition by range (date)
( start (date '2017-01-01') inclusive
end (date '2018-01-01') exclusive
every (interval '1 month') );
insert into sales2 select * from sales;
drop table sales;
alter table sales2 rename to sales;
analyze sales;
grant all privileges on sales to admin;
grant select on sales to guest;
select partitionboundary, partitiontablename, partitionname,partitionlevel, partitionrank
from pg_partitions
where tablename='sales';
以下表和视图提供了分区表的信息。
create table sales (id int, year int, month int, day int,
region text)
distributed by (id)
partition by range (year)
subpartition by range (month)
subpartition template (
start (1) end (13) every (1),
default subpartition other_months )
subpartition by list (region)
subpartition template (
subpartition usa values ('北京'),
subpartition europe values ('上海'),
subpartition asia values ('广州'),
default subpartition other_regions )
( start (2017) end (2020) every (1),
default partition outlying_years );
sales表最底层存储数据的分区共有 4 * 13 * 4 = 208个;
图2
2. 无条件查询,查询计划如图3所示。图3
可以看到,该查询扫描了全部208个分区,没有分区消除。图4
可以看到,该查询扫描了全部208个分区的一半,104个分区。顶级年份分区有四个,为什么where year='2017'要扫描104而不是52个分区呢?在运行时,查询优化器会扫描这个表的层级关系(系统表),并使用CHECK表约束确定扫描哪些满足查询条件的分区。如果存在DEFAULT分区,则它总是被扫描,因此该查询或扫描year=2017和default两个分区,这就是扫描的分区数是104而不是52的原因。可见,包含DEFAULT分区会增加整体扫描时间。按理说DEFAULT与其它所有分区的数据都是互斥的,完全不必在可以确定分区的条件下再去扫描它,这是不是HAWQ查询优化器的一个问题也未可知。图5
可以看到,这次只扫描了16个分区。同样道理本应只扫描4个底层分区,因为DEFAULT的存在,需要扫描16个分区。图6
这次只需扫描一个分区。当查询中包含所有层级的谓词条件时,没有扫描DEFAULT,而是唯一确定了一个分区。图7
这次只要扫描年份DEFAULT分区下的52个子分区。create table sales (id int, year int, month int, day int,
region text)
distributed by (id)
partition by range (year)
subpartition by range (month)
subpartition template (
start (1) end (13) every (1),
default subpartition other_months )
subpartition by list (region)
subpartition template (
subpartition usa values ('北京'),
subpartition europe values ('上海'),
subpartition asia values ('广州'),
default subpartition other_regions )
( start (2017) end (2020) every (1));
alter table sales add partition
start (2016) inclusive
end (2017) exclusive;
使用add partition增加分区时不能存在DEFAULT分区,否则会报类似以下的错误:
ERROR: cannot add RANGE partition to relation "sales" with DEFAULT partition "outlying_years"
HINT: need to SPLIT partition "outlying_years"
这时需要使用split partition增加分区。
alter table sales alter partition for (rank(12))
add partition africa values ('africa');
alter table sales alter partition for (rank(1))
add partition africa values ('africa');
alter table sales add default partition other;
如果没有DEFAULT分区,不能匹配分区CHECK约束的数据行将被拒绝入库,并且数据转载失败。为了避免这种情况,指定DEFAULT分区。任何不能与分区匹配的行都被装载进DEFAULT分区。
__prt_
例如:
sales_1_prt_1_2_prt_11_3_prt_other_regions
上面的名称表示该分区名为'other_regions',是sales表的一个第三级分区,隶属第一级的1号分区下的第二级的11号分区下。
alter table sales rename to globalsales;
相关的分区子表名变为:
globalsales_1_prt_1_2_prt_11_3_prt_other_regions
也可以将顶级分区名改为自定义的名称,例如:
alter table sales rename partition for (2017) to y2017;
表对象名的最大长度为64字节,超长会报错:
db1=# alter table globalsales rename partition for (2017) to year2017;
ERROR: relation name "globalsales_1_prt_year2017_2_prt_other_months_3_prt_other_regions" for child partition is too long
当使用ALTER TABLE...PARTITION 命令修改分区表时,总是用分区名称(如y2017)而不是分区对应的表对象全名(globalsales_1_prt_y2017)。
alter table globalsales drop partition for (2017);
alter table globalsales drop partition for (2018);
不能删除最后一个分区:
db1=# alter table globalsales drop partition for (2019);
ERROR: cannot drop partition for value (2019) of relation "globalsales" -- only one remains
HINT: Use DROP TABLE "globalsales" to remove the table and the final partition
alter table globalsales truncate partition for (2018);
db1=# alter table sales exchange partition for (2017)
db1-# with table stage_sales;
ERROR: cannot EXCHANGE PARTITION for relation "sales" -- partition has children
经常使用分区交换向分区表装载数据。当然也能使用COPY或INSERT命令向分区表装载数据,此时数据被自动路由到正确的底层分区,就像普通表一样。但是,这种装载数据的方法会根据数据遍历整个分区层次结构,因此数据装载的性能很差。在前面208个分区的例子中,插入一条记录竟然用时16秒多,如图8所示。
图8
向分区表装载数据的推荐方法创建一个中间过渡表,装载过渡表,然后用过渡表与分区做交换。db1=# create table sales (id int, year int, month int, day int, region varchar(10))
db1-# distributed by (id)
db1-# partition by range (year)
db1-# ( start (2017) end (2020) every (1));
NOTICE: CREATE TABLE will create partition "sales_1_prt_1" for table "sales"
NOTICE: CREATE TABLE will create partition "sales_1_prt_2" for table "sales"
NOTICE: CREATE TABLE will create partition "sales_1_prt_3" for table "sales"
CREATE TABLE
Time: 497.864 ms
db1=# insert into sales values (1,2017,1,1,'北京');
INSERT 0 1
Time: 463.546 ms
db1=# insert into sales values (2,2018,2,2,'上海');
INSERT 0 1
Time: 133.454 ms
db1=# insert into sales values (3,2019,3,3,'广州');
INSERT 0 1
Time: 109.118 ms
db1=# create table stage_sales (like sales);
NOTICE: Table doesn't have 'distributed by' clause, defaulting to distribution columns from LIKE table
CREATE TABLE
Time: 130.794 ms
db1=# \dt;
List of relations
Schema | Name | Type | Owner | Storage
--------+---------------+-------+---------+-------------
public | sales | table | gpadmin | append only
public | sales_1_prt_1 | table | gpadmin | append only
public | sales_1_prt_2 | table | gpadmin | append only
public | sales_1_prt_3 | table | gpadmin | append only
public | stage_sales | table | gpadmin | append only
(5 rows)
db1=# insert into stage_sales values (4,2017,4,4,'深圳');
INSERT 0 1
Time: 1559.465 ms
db1=# alter table sales exchange partition for (2017) with table stage_sales;
ALTER TABLE
Time: 61.744 ms
db1=# select * from sales;
id | year | month | day | region
----+------+-------+-----+--------
2 | 2018 | 2 | 2 | 上海
3 | 2019 | 3 | 3 | 广州
4 | 2017 | 4 | 4 | 深圳
(3 rows)
Time: 91.150 ms
db1=# select * from stage_sales;
id | year | month | day | region
----+------+-------+-----+--------
1 | 2017 | 1 | 1 | 北京
(1 row)
Time: 82.853 ms
db1=# alter table sales split partition for (2017)
db1-# at (2016)
db1-# into (partition y016, partition y2017);
ERROR: cannot split partition with child partitions
HINT: Try splitting the child partitions.
下面的例子将2017年1月的分区,分割成2017年1月1日到2017年1月15日、2017年1月16日到2017年1月31日两个分区,分割值包含在后一个分区中。
db1=# create table sales (id int, date date, amt decimal(10,2))
db1-# distributed by (id)
db1-# partition by range (date)
db1-# ( partition p201701 start (date '2017-01-01') inclusive ,
db1(# partition p201702 start (date '2017-02-01') inclusive
db1(# end (date '2017-03-01') exclusive );
NOTICE: CREATE TABLE will create partition "sales_1_prt_p201701" for table "sales"
NOTICE: CREATE TABLE will create partition "sales_1_prt_p201702" for table "sales"
CREATE TABLE
Time: 274.237 ms
db1=# insert into sales values (1, date '2017-01-15', 100);
INSERT 0 1
Time: 386.221 ms
db1=# insert into sales values (1, date '2017-01-16', 100);
INSERT 0 1
Time: 146.437 ms
db1=# select * from sales_1_prt_p201701;
id | date | amt
----+------------+--------
1 | 2017-01-15 | 100.00
1 | 2017-01-16 | 100.00
(2 rows)
Time: 117.187 ms
db1=# alter table sales split partition for ('2017-01-01') at ('2017-01-16')
db1-# into (partition p20170101to0115, partition p20170116to0131);
NOTICE: exchanged partition "p201701" of relation "sales" with relation "pg_temp_68011"
NOTICE: dropped partition "p201701" for relation "sales"
NOTICE: CREATE TABLE will create partition "sales_1_prt_p20170101to0115" for table "sales"
NOTICE: CREATE TABLE will create partition "sales_1_prt_p20170116to0131" for table "sales"
ALTER TABLE
Time: 446.998 ms
db1=# select * from sales_1_prt_p20170101to0115;
id | date | amt
----+------------+--------
1 | 2017-01-15 | 100.00
(1 row)
Time: 132.169 ms
db1=# select * from sales_1_prt_p20170116to0131;
id | date | amt
----+------------+--------
1 | 2017-01-16 | 100.00
(1 row)
Time: 86.589 ms
如果表有DEFAULT分区,必须使用分裂分区的方法添加分区。使用INTO子句的第二个分区为DEFAULT分区。
db1=# alter table sales add default partition other;
NOTICE: CREATE TABLE will create partition "sales_1_prt_other" for table "sales"
ALTER TABLE
Time: 134.470 ms
db1=# insert into sales values (3, date '2017-03-01', 100);
INSERT 0 1
Time: 242.053 ms
db1=# insert into sales values (4, date '2017-04-01', 100);
INSERT 0 1
Time: 147.235 ms
db1=# select * from sales_1_prt_other;
id | date | amt
----+------------+--------
4 | 2017-04-01 | 100.00
3 | 2017-03-01 | 100.00
(2 rows)
Time: 79.584 ms
db1=# alter table sales split default partition
db1-# start ('2017-03-01') inclusive
db1-# end ('2017-04-01') exclusive
db1-# into (partition p201703, default partition);
NOTICE: exchanged partition "other" of relation "sales" with relation "pg_temp_68051"
NOTICE: dropped partition "other" for relation "sales"
NOTICE: CREATE TABLE will create partition "sales_1_prt_p201703" for table "sales"
NOTICE: CREATE TABLE will create partition "sales_1_prt_other" for table "sales"
ALTER TABLE
Time: 756.526 ms
db1=# select * from sales_1_prt_p201703;
id | date | amt
----+------------+--------
3 | 2017-03-01 | 100.00
(1 row)
Time: 89.353 ms
db1=# select * from sales_1_prt_other;
id | date | amt
----+------------+--------
4 | 2017-04-01 | 100.00
(1 row)
Time: 69.030 ms
db1=# create table sales (trans_id int, date date, amount decimal(9,2), region text)
db1-# distributed by (trans_id)
db1-# partition by range (date)
db1-# subpartition by list (region)
db1-# subpartition template
db1-# ( subpartition usa values ('usa'),
db1(# subpartition asia values ('asia'),
db1(# subpartition europe values ('europe'),
db1(# default subpartition other_regions )
db1-# ( start (date '2017-01-01') inclusive
db1(# end (date '2017-04-01') exclusive
db1(# every (interval '1 month') );
NOTICE: CREATE TABLE will create partition "sales_1_prt_1" for table "sales"
...
CREATE TABLE
Time: 623.565 ms
db1=# alter table sales set subpartition template
db1-# ( subpartition usa values ('usa'),
db1(# subpartition asia values ('asia'),
db1(# subpartition europe values ('europe'),
db1(# subpartition africa values ('africa'),
db1(# default subpartition regions );
NOTICE: replacing level 1 subpartition template specification for relation "sales"
ALTER TABLE
Time: 49.767 ms
当添加一个分区时,使用新的子分区模板。
db1=# alter table sales add partition "4"
db1-# start ('2017-04-01') inclusive
db1-# end ('2017-05-01') exclusive ;
NOTICE: CREATE TABLE will create partition "sales_1_prt_4" for table "sales"
...
ALTER TABLE
Time: 414.251 ms
db1=# \dt sales*
List of relations
Schema | Name | Type | Owner | Storage
--------+-----------------------------------+-------+---------+-------------
public | sales | table | gpadmin | append only
public | sales_1_prt_1 | table | gpadmin | append only
public | sales_1_prt_1_2_prt_asia | table | gpadmin | append only
public | sales_1_prt_1_2_prt_europe | table | gpadmin | append only
public | sales_1_prt_1_2_prt_other_regions | table | gpadmin | append only
public | sales_1_prt_1_2_prt_usa | table | gpadmin | append only
public | sales_1_prt_2 | table | gpadmin | append only
public | sales_1_prt_2_2_prt_asia | table | gpadmin | append only
public | sales_1_prt_2_2_prt_europe | table | gpadmin | append only
public | sales_1_prt_2_2_prt_other_regions | table | gpadmin | append only
public | sales_1_prt_2_2_prt_usa | table | gpadmin | append only
public | sales_1_prt_3 | table | gpadmin | append only
public | sales_1_prt_3_2_prt_asia | table | gpadmin | append only
public | sales_1_prt_3_2_prt_europe | table | gpadmin | append only
public | sales_1_prt_3_2_prt_other_regions | table | gpadmin | append only
public | sales_1_prt_3_2_prt_usa | table | gpadmin | append only
public | sales_1_prt_4 | table | gpadmin | append only
public | sales_1_prt_4_2_prt_africa | table | gpadmin | append only
public | sales_1_prt_4_2_prt_asia | table | gpadmin | append only
public | sales_1_prt_4_2_prt_europe | table | gpadmin | append only
public | sales_1_prt_4_2_prt_regions | table | gpadmin | append only
public | sales_1_prt_4_2_prt_usa | table | gpadmin | append only
(22 rows)
下面的命令移除子分区模板:
alter table sales set subpartition template ();
create table sales (id int, date date, amt decimal(10,2))
distributed by (id)
partition by range (date)
( start (date '2016-03-01') inclusive
end (date '2017-05-01') exclusive
every (interval '1 day') );
该语句建立了从2016-03-01至2017-04-30的每天一个分区。
create or replace function fn_rolling_partition() returns int
as $body$
declare
oldest_month_first_day date := date(date_trunc('month',current_date) + interval '-13 month');
oldest_month_last_day date := date(date_trunc('month',current_date) + interval '-12 month - 1 day');
newest_month_first_day date := date(date_trunc('month',current_date) + interval '1 month');
newest_month_last_day date := date(date_trunc('month',current_date) + interval '2 month - 1 day');
i int;
j int;
sqlstring varchar(1000);
begin
-- 转储最早一个月的数据,
sqlstring = 'copy (select * from sales where date >= date(''' || oldest_month_first_day || ''') and date <= date(''' || oldest_month_last_day || ''')) to ''/home/gpadmin/sales_' || to_char(oldest_month_first_day,'YYYYMM') || '.txt'' with delimiter ''|'';';
execute sqlstring;
-- raise notice '%', sqlstring;
-- 删除最早月份对应的分区
i := 1;
j := oldest_month_last_day - oldest_month_first_day + 1;
for i in 1 .. j loop
sqlstring := 'alter table sales drop partition for (rank('|| i ||'));';
execute sqlstring;
end loop;
-- 增加下一个月份的新分区
while newest_month_first_day <= newest_month_last_day loop
sqlstring := 'alter table sales add partition start (date '''|| newest_month_first_day ||''') inclusive end (date '''|| (newest_month_first_day + 1) ||''') exclusive;';
execute sqlstring;
-- raise notice '%', sqlstring;
newest_month_first_day = newest_month_first_day + 1;
end loop;
-- 正常返回1
return 1;
-- 异常返回0
exception when others then
raise exception '%: %', sqlstate, sqlerrm;
return 0;
end
$body$ language plpgsql;
0 2 1 * * psql -d db1 -c "select fn_rolling_partition();" > rolling_partition.log 2>&1