表分区让我们能通过把表划分成较小的、更容易管理的小块来支持非常大的表,例如事实表。通过让 Greenplum 数据库查询优化器只扫描满足给定查询所需的数据而避免扫描大表的全部内容,分区表能够提升查询性能。
关于表分区
分区表就是将一个大表在物理上分割成若干小表,并且整个过程对用户是透明的,也就是用户的所有操作仍然是作用在大表上,不需要关心数据实际上落在哪张小表里面。Greenplum 中分区表的原理和 PostgreSQL 一样,都是通过表继承和约束实现的。
分区并不会改变表数据在 Segment 节点之间的物理分布。
Greenplum 数据库支持:
- 范围分区:基于一个数值型范围划分数据,例如按日期区间定义
- 列表分区:基于一个值列表划分数据,例如按城市属性定义
- 多级分区表:范围分区和列表分区的多级组合
分区与分布
- 分布:DISTRIBUTED
- 分区:PARTITION
Greenplum 中每个表都需要有一个分布键,如果你建表的时候没有显示使用语法 DISTRIBUTED BY (column)
指定一个分布键,系统也会默认为你指定一个。分布的目的是把数据划分到每个 Segment 节点,划分的规则是 hash 或者 randonly。这样在计算时可以充分利用每个节点的资源进行并行计算。
分区与分布的区别如下:
- 表分布是物理的,Greenplum 数据库会在物理上把分区表和非分区表划分到多个 Segment 节点上来启用并行查询处理
- 表分区是逻辑的,Greenplum 数据库在逻辑上拆分大表来提升查询性能并且有利于数据仓库的维护任务
- 表分区不会影响数据在各个 Segment 节点上的分布情况
什么时候使用分区表
是否启用分区表,可以通过以下几个方面进行考虑:
- 表数据量是否足够大:通常对于较大的事实表,比如数据量有几千万或者过亿,我们可以考虑使用分区表,但数据量大小并没有一个绝对的标准可以使用,一般是根据经验,以及对目前性能是否满意
- 表是否有合适的分区字段:如果数据量足够大了,这个时候我们就需要看下是否有合适的字段能够用来分区,通常如果数据有时间维度,比如按天、按月、按年等,是比较理想的分区字段
- 表内数据是否具有生命周期:通常数仓中的数据不可能一直存放,一般都会有一定的生命周期,比如最近一年等,这里就涉及到对旧数据的管理,如果有分区表,就很容易删除旧的数据,或者将旧的数据归档到对象存储等更为廉价的存储介质上
- 查询语句中是否含有分区字段:如果你对一个表做了分区,但是所有的查询都不带分区字段(无法利用分区裁剪),这不仅无法提高性能反而会使性能下降,因为所有的查询都会扫描所有的分区表
注:
- 不要创建超过所需数量的分区,创建过多的分区可能会拖慢管理和维护工作,例如清理、恢复 Segment、扩展集群、检查磁盘用量等等
- 除非查询优化器能基于查询谓词排除一些分区,否则分区技术就不能改进查询性能。每个分区都扫描的查询,运行起来会比表没有分区时还慢,因此如果用户的查询中很少能实现分区裁剪,请避免进行分区。可以通过查询 SQL 的执行计划来确认分区是否被裁剪
创建分区表
在使用 CREATE TABLE
创建表时就可以对它们分区。
范围分区
用户可以通过指定一个起始值(START)、一个结束值(END)以及一个定义分区增量值的子句让数据库自动产生分区。默认情况下,起始值总是在当前分区中而结束值总是在下个分区中。
- inclusive 指定包含:例如下面的
start (date'2020-01-01') inclusive
则是包含 '2020-01-01' - exclusive 指定不包含, 例如下面的
end (date'2025-01-01')
则是不包含 '2025-01-01'
# 根据选定的范围,跨越基数,快速分区每一个子表 create table sales (id int, date date, amt decimal(10,2)) distributed by (id) partition by range(date) ( start (date'2020-01-01') inclusive end (date'2025-01-01') exclusive every (interval '1 year') ); -- insert values insert into sales values(1, date'2020-01-01', 10); insert into sales values(2, date'2021-01-01', 20); insert into sales values(3, date'2022-01-01', 30); testadb=> select * from sales order by id; id | date | amt ----+------------+------- 1 | 2020-01-01 | 10.00 2 | 2021-01-01 | 20.00 3 | 2022-01-01 | 30.00 (3 rows) schemaname | tablename | partitiontablename | partitionname | partitiontype | partitionrangestart | partitionrangeend ------------+-----------+--------------------+---------------+---------------+---------------------+-------------------- public | sales | sales_1_prt_1 | | range | '2020-01-01'::date | '2021-01-01'::date public | sales | sales_1_prt_2 | | range | '2021-01-01'::date | '2022-01-01'::date public | sales | sales_1_prt_3 | | range | '2022-01-01'::date | '2023-01-01'::date public | sales | sales_1_prt_4 | | range | '2023-01-01'::date | '2024-01-01'::date public | sales | sales_1_prt_5 | | range | '2024-01-01'::date | '2025-01-01'::date (5 rows)
当 insert
的数据大于最大分区或小于最小分区时,会提示如下错误:
testadb=> insert into sales values(4, date'2019-01-01', 50); ERROR: no partition for partitioning key (seg0 172.16.0.158:4000 pid=3591) testadb=> insert into sales values(5, date'2025-01-01', 60); ERROR: no partition for partitioning key (seg1 172.16.0.157:4000 pid=4032)
在创建分区表时,通过指定 default partition
可解决上述报错:
create table sales_extra (id int, date date, amt decimal(10,2)) distributed by (id) partition by range(date) ( start (date'2020-01-01') inclusive end (date'2025-01-01') exclusive every (interval '1 year'), default partition extra ); -- insert values insert into sales_extra values(1, date'2020-01-01', 10); insert into sales_extra values(2, date'2021-01-01', 20); insert into sales_extra values(3, date'2022-01-01', 30); insert into sales_extra values(4, date'2019-01-01', 50); insert into sales_extra values(5, date'2025-01-01', 60); testadb=> select * from sales_extra order by id; id | date | amt ----+------------+------- 1 | 2020-01-01 | 10.00 2 | 2021-01-01 | 20.00 3 | 2022-01-01 | 30.00 4 | 2019-01-01 | 50.00 5 | 2025-01-01 | 60.00 (5 rows) # 指定分区查询 testadb=> select * from sales_extra_1_prt_extra; id | date | amt ----+------------+------- 4 | 2019-01-01 | 50.00 5 | 2025-01-01 | 60.00 (2 rows)
序号 | 分区方式 | 描述 |
1 | 按天分区 | every (interval '1 day') |
2 | 按月分区 | every (interval '1 month') |
3 | 按年分区 | every (interval '1 year') |
用户也可以逐个声明并且命名每一个分区,不需要为每一个分区声明一个 END 子句。例如:
create table sales (id int, date date, amt decimal(10,2)) distributed by (id) partition by range(date) ( partition p2020 start (date'2020-01-01') inclusive, partition p2021 start (date'2021-01-01') inclusive, partition p2022 start (date'2022-01-01') inclusive, partition p2023 start (date'2023-01-01') inclusive, partition p2024 start (date'2024-01-01') inclusive end (date'2025-01-01') exclusive ); -- insert values insert into sales values(1, date'2020-01-01', 10); insert into sales values(2, date'2021-01-01', 20); insert into sales values(3, date'2022-01-01', 30); testadb=> select * from sales order by id; id | date | amt ----+------------+------- 1 | 2020-01-01 | 10.00 2 | 2021-01-01 | 20.00 3 | 2022-01-01 | 30.00 (3 rows)
当 insert
的数据或小于最小分区时,会提示如下错误:
testadb=> insert into sales values(4, date'2019-01-01', 50); ERROR: no partition for partitioning key (seg0 172.16.0.158:4000 pid=6713)
在创建分区表时,为第一个分区声明一个 END
子句,可解决上述报错:
create table sales (id int, date date, amt decimal(10,2)) distributed by (id) partition by range(date) ( partition pmin start (date'1900-01-01') inclusive end (date'2020-01-01') exclusive, partition p2020 start (date'2020-01-01') inclusive, partition p2021 start (date'2021-01-01') inclusive, partition p2022 start (date'2022-01-01') inclusive, partition p2023 start (date'2023-01-01') inclusive, partition p2024 start (date'2024-01-01') inclusive end (date'2025-01-01') exclusive ); -- insert values insert into sales values(1, date'2020-01-01', 10); insert into sales values(2, date'2021-01-01', 20); insert into sales values(3, date'2022-01-01', 30); insert into sales values(4, date'2019-01-01', 50); # 指定分区查询 testadb=> select * from sales_1_prt_pmin; id | date | amt ----+------------+------- 4 | 2019-01-01 | 50.00 (1 row)
创建一个按数字范围分区的表,例如使用 int 类型的列作为分区键列,示例 SQL 如下:
create table rank (id int, rank int, gender varchar(1), count int) distributed by (id) partition by range(rank) ( start (2020) end (2025) every (1), default partition extra );
列表分区
LIST 分区表可以使用任意允许值比较的列作为分区键列。创建 LIST 分区表时,必须要为每一个分区声明每一个值分区。
create table list_example (id int, state_cd varchar(2), data varchar(20)) distributed by (id) partition by list(state_cd) ( partition p1 values ('ME', 'NH', 'VT', 'MA'), partition p2 values ('CT', 'RI', 'NY'), default partition other ); -- insert values insert into list_example values(1, 'VT', 'App data...'); insert into list_example values(2, 'NY', 'App data...'); testadb=> select * from list_example; id | state_cd | data ----+----------+------------- 1 | VT | App data... 2 | NY | App data... (2 rows)
创建一个按字符串列表分区的表,示例 SQL 如下:
create table list_example (id int, date varchar(10), amt decimal(10,2)) distributed by (id) partition by list(date) ( partition p1 values ('2020-01-01', '2020-01-02'), partition p2 values ('2020-01-03'), default partition other ); -- insert values insert into list_example values(1, '2020-01-01', 10); insert into list_example values(2, '2020-01-03', 20); insert into list_example values(3, '2020-01-05', 30); testadb=> select * from list_example order by id; id | date | amt ----+------------+------- 1 | 2020-01-01 | 10.00 2 | 2020-01-03 | 20.00 3 | 2020-01-05 | 30.00 (3 rows)
多级分区
- 创建二级分区(一级分区在 date 字段上进行 RANGE 分区,二级分区在 region 字段上进行 LIST 分区)
create table p2_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 europe values ('europe'), default subpartition other_regions ) ( start (date'2020-01-01') inclusive end (date'2020-05-01') exclusive every (interval '1 month'), default partition outlying_dates );
- 创建三级分区(一级分区在 year 字段上进行 RANGE 分区,二级分区在 month 字段上进行 RANGE 分区,三级分区在 region 字段上进行 LIST 分区)
create table p3_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 (2020) end (2025) every(1), default partition outlying_years );
维护分区表
分区表也是一张表,所以对于表的很多操作也可以作用于分区表上。
- 创建示例分区表
create table sales_example (id int, date date, amt decimal(10,2)) distributed by (id) partition by range(date) ( partition p2020 start (date'2020-01-01') inclusive, partition p2021 start (date'2021-01-01') inclusive end (date'2022-01-01') exclusive );
- 查看分区信息
select t.schemaname, t.tablename, t.partitiontablename, t.partitionname, t.partitiontype, t.partitionrangestart, t.partitionrangeend from pg_partitions t where t.schemaname = 'public' and t.tablename = 'sales_example'; schemaname | tablename | partitiontablename | partitionname | partitiontype | partitionrangestart | partitionrangeend ------------+---------------+---------------------------+---------------+---------------+---------------------+-------------------- public | sales_example | sales_example_1_prt_p2020 | p2020 | range | '2020-01-01'::date | '2021-01-01'::date public | sales_example | sales_example_1_prt_p2021 | p2021 | range | '2021-01-01'::date | '2022-01-01'::date (2 rows)
增加一个分区
可以通过 ALTER TABLE
语句为分区表增加一个分区。如果创建分区表时使用了子分区模板,那么新增的分区也会根据该模板划分子分区。
- 增加一个分区,并指定分区名
alter table sales_example add partition p2022 start (date'2022-01-01') inclusive end (date'2023-01-01') exclusive; NOTICE: CREATE TABLE will create partition "sales_example_1_prt_p2022" for table "sales_example" ALTER TABLE
- 查看分区信息
select t.schemaname, t.tablename, t.partitiontablename, t.partitionname, t.partitiontype, t.partitionrangestart, t.partitionrangeend from pg_partitions t where t.schemaname = 'public' and t.tablename = 'sales_example'; schemaname | tablename | partitiontablename | partitionname | partitiontype | partitionrangestart | partitionrangeend ------------+---------------+---------------------------+---------------+---------------+---------------------+-------------------- public | sales_example | sales_example_1_prt_p2020 | p2020 | range | '2020-01-01'::date | '2021-01-01'::date public | sales_example | sales_example_1_prt_p2021 | p2021 | range | '2021-01-01'::date | '2022-01-01'::date public | sales_example | sales_example_1_prt_p2022 | p2022 | range | '2022-01-01'::date | '2022-03-01'::date (3 rows)
注:不支持对有默认分区的分区表增加分区,如需增加分区需通过分裂默认分区的方法来增加分区。
增加一个默认分区
用 ALTER TABLE
命令可以为一个分区表增加一个默认分区。
testadb=> alter table sales_example add default partition other; NOTICE: CREATE TABLE will create partition "sales_example_1_prt_other" for table "sales_example" ALTER TABLE
- 插入一条记录
date'1900-01-01'
,将会写入默认分区中
testadb=> insert into sales_example values (1, date'1900-01-01', 10); INSERT 0 1 testadb=> select * from sales_example_1_prt_other; id | date | amt ----+------------+------- 1 | 1900-01-01 | 10.00 (1 row)
如果 insert
的数据不匹配一个分区的 CHECK
约束并且没有默认分区,该数据就会被拒绝。默认分区能确保 insert
的不匹配一个分区的数据能被插入到默认分区中。
重命名一个分区
分区表默认使用下列命名规范:
<parentname>_<level>_prt_<partition_name>
例如:
sales_example_1_prt_p2020
对于自动生成的范围分区,在没有给出分区名称时会分配一个数字:
sales_example_1_prt_1
使用如下语句,更改一个分区的名称:
testadb=> alter table sales_example rename partition for ('2022-01-01'::date) to p2022; ALTER TABLE
删除一个分区
使用 ALTER TABLE
命令从分区表中删除一个分区。当删除一个具有子分区的分区时,子分区(以及其中的所有数据)也会被自动删除。
testadb=> alter table sales_example drop partition p2022; ALTER TABLE
清空一个分区
使用 ALTER TABLE
命令清空一个分区。当清空一个具有子分区的分区时,子分区也会被自动清空。
testadb=> alter table sales_example truncate partition p2021; ALTER TABLE
交换一个分区
交换分区技术,主要是将一个非分区表的数据与一个分区表的一个分区进行数据交换。
testadb=> insert into sales_example values (1, '2022-01-31'::date, 10); INSERT 0 1 testadb=> create table p2022 as select * from sales_example where 1 = 2 distributed by (id); SELECT 0 testadb=> insert into p2022 values (1, '2022-10-31'::date, 10); INSERT 0 1 testadb=> alter table sales_example exchange partition for ('2022-01-01'::date) with table p2022; NOTICE: exchanged partition "p2022" of relation "sales_example" with relation "p2022" ALTER TABLE testadb=> select * from sales_example_1_prt_p2022; id | date | amt ----+------------+------- 1 | 2022-10-31 | 10.00 (1 row) testadb=> select * from p2022; id | date | amt ----+------------+------- 1 | 2022-01-31 | 10.00 (1 row)
分裂一个分区
分裂一个分区会把一个分区划分成两个分区。
分裂分区存在如下限制:
- 仅支持分裂最底层的分区,即只有包含数据的分区可以被分裂
- 分裂分区语句中指定的分裂值会被分在后一个分区中
将 2022 年的分区分裂成两个分区,一个分区包含1月1号至6月30号,第二个分区包含7月1号至12月31号,分裂分区的示例语句如下:
alter table sales_example split partition for ('2022-01-01'::date) at ('2022-07-01'::date) into (partition p2022s1, partition p2022s2); schemaname | tablename | partitiontablename | partitionname | partitiontype | partitionrangestart | partitionrangeend ------------+---------------+-----------------------------+---------------+---------------+---------------------+-------------------- public | sales_example | sales_example_1_prt_p2020 | p2020 | range | '2020-01-01'::date | '2021-01-01'::date public | sales_example | sales_example_1_prt_p2021 | p2021 | range | '2021-01-01'::date | '2022-01-01'::date public | sales_example | sales_example_1_prt_p2022s1 | p2022s1 | range | '2022-01-01'::date | '2022-07-01'::date public | sales_example | sales_example_1_prt_p2022s2 | p2022s2 | range | '2022-07-01'::date | '2023-01-01'::date (4 rows)
如果分区表中拥有一个默认分区,可以使用分裂默认分区的方法来增加分区。在使用 INTO 子句时,您需要指定当前的默认分区为第二个分区名。分裂默认分区的示例语句如下:
- 添加默认分区
alter table sales_example add default partition other;
- 分裂默认分区
alter table sales_example split default partition start ('2023-01-01'::date) inclusive end ('2024-01-01'::date) exclusive into (partition p2023, default partition); schemaname | tablename | partitiontablename | partitionname | partitiontype | partitionrangestart | partitionrangeend ------------+---------------+-----------------------------+---------------+---------------+---------------------+-------------------- public | sales_example | sales_example_1_prt_p2020 | p2020 | range | '2020-01-01'::date | '2021-01-01'::date public | sales_example | sales_example_1_prt_p2021 | p2021 | range | '2021-01-01'::date | '2022-01-01'::date public | sales_example | sales_example_1_prt_p2022s1 | p2022s1 | range | '2022-01-01'::date | '2022-07-01'::date public | sales_example | sales_example_1_prt_p2022s2 | p2022s2 | range | '2022-07-01'::date | '2023-01-01'::date public | sales_example | sales_example_1_prt_p2023 | p2023 | range | '2023-01-01'::date | '2024-01-01'::date public | sales_example | sales_example_1_prt_other | other | range | | (6 rows)
分区裁剪
Greenplum 支持分区表的分区裁剪功能,根据查询条件会只扫描所需的数据分区而避免扫描整个表的全部内容,提升查询性能。
以表 sales_example 为例,该表按日期范围分区,对于下列查询:
testadb=> explain select * from sales_example where date = date'2022-10-31'; QUERY PLAN ------------------------------------------------------------------------------------------- Gather Motion 2:1 (slice1; segments: 2) (cost=0.00..1715.00 rows=122 width=24) -> Append (cost=0.00..1715.00 rows=61 width=24) -> Seq Scan on sales_example_1_prt_p2022s2 (cost=0.00..857.50 rows=31 width=24) Filter: (date = '2022-10-31'::date) -> Seq Scan on sales_example_1_prt_other (cost=0.00..857.50 rows=31 width=24) Filter: (date = '2022-10-31'::date) Optimizer: Postgres query optimizer
如上执行计划所示,该表有好几个分区,只需要读取二个分区(默认分区和当前分区)。
分区表数据字典
pg_partition
一个表是否是分区表保存在 pg_partition
中,如果一个表是分区表(不包括子分区),则对应有一条记录在这个数据字典中。pg_partition
表结构如下所示:
字段名 | 类型 | 引用 | 字段说明 |
oid | oid | 唯一标识分区表的 oid | |
parrelid | oid | pg_class.oid | 表对应在 pg_class 中的 oid |
parkind | char | 分区类型,R=Range 模式,L=List 模式 | |
parlevel | smallint | 分区的层级,0 表示分区表的父表,1 表示第一层分区,2 表示第二层分区 | |
paristemplate | boolean | 该分区是否为子分区模板或者是确定层级的分区 | |
parnatts | smallint | pg_attribute.oid | 分区键的个数 |
paratts | int2vector | 分区键中对应 pg_attribute 中的 attnum 的列表 | |
parclass | oidvector | pg_opclass.oid | 跟 pg_class 关联,定义分区键的操作方法 |
如果想查询一个表是否是分区表,只要将 pg_partition
与 pg_class
关联,然后执行 count
即可。
testadb=> select count(*) from pg_partition where parrelid = 'public.sales'::regclass; count ------- 1 (1 row) select * from pg_partition t, pg_class p where t.parrelid = p.oid and p.relname = 'sales';
pg_partition_rule
分区表的分区规则保存在 pg_partition_rule
中。在这个表中,可以找到一个分区表对应的子表有哪些及分区规则等信息。pg_partition_rule
表结构如下所示:
字段名 | 类型 | 引用 | 字段说明 |
oid | oid | 唯一标识的 oid | |
paroid | oid | pg_partition.oid | 与 pg_partition 关联,对应其 oid |
parchildrelid | oid | pg_class.oid | 分区表子表对应 pg_class 的 oid |
parparentrule | oid | pg_partition_rule.paroid | 父表对应的分区的 paroid |
parname | name | 子分区名 | |
parisdefault | boolean | 该分区是否为默认分区 | |
parruleord | smallint | 对于 range 分区,对于的分区序号 | |
parrangestartincl | boolean | 对于 range 分区,该分区是否包括起始值 | |
parrangeendincl | boolean | 对于 range 分区,该分区是否包括结束值 | |
parrangestart | text | 起始值 | |
parrangeend | text | 结束值 | |
parrangeevery | text | 对于 range 分区,每一个分区的间隔值 | |
parlistvalues | text | 对于 list 分区,该分区对应的数值 | |
parreloptions | text[] | 描述特定分区的存储特性 |
select m.parname, * from pg_partition t, pg_class p, pg_partition_rule m where t.parrelid = p.oid and m.paroid = t.oid and p.relname = 'sales';
pg_partitions
在 Greenplum 中,定义了一个 pg_partitions
的视图,方便对分区表进行查看。
select t.schemaname, t.tablename, t.partitiontablename, t.partitionname, t.partitiontype, t.partitionrangestart, t.partitionrangeend, t.partitionboundary from pg_partitions t where t.schemaname = 'public' and t.tablename = 'sales'; schemaname | tablename | partitiontablename | partitionname | partitiontype | partitionrangestart | partitionrangeend ------------+-----------+--------------------+---------------+---------------+---------------------+-------------------- public | sales | sales_1_prt_pmin | pmin | range | '1900-01-01'::date | '2020-01-01'::date public | sales | sales_1_prt_p2020 | p2020 | range | '2020-01-01'::date | '2021-01-01'::date public | sales | sales_1_prt_p2021 | p2021 | range | '2021-01-01'::date | '2022-01-01'::date public | sales | sales_1_prt_p2022 | p2022 | range | '2022-01-01'::date | '2023-01-01'::date public | sales | sales_1_prt_p2023 | p2023 | range | '2023-01-01'::date | '2024-01-01'::date public | sales | sales_1_prt_p2024 | p2024 | range | '2024-01-01'::date | '2050-01-01'::date (6 rows)
pg_partition_columns
显示在一个分区设计中用到的分区键列:
testadb=> select * from pg_partition_columns; schemaname | tablename | columnname | partitionlevel | position_in_partition_key ------------+--------------+------------+----------------+--------------------------- public | sales_extra | date | 0 | 1 public | sales | date | 0 | 1 public | list_example | date | 0 | 1 public | p2_sales | date | 0 | 1 public | p2_sales | region | 1 | 1 public | p2_sales | region | 1 | 1 (6 rows)
参考资料
https://help.aliyun.com/document_detail/118173.html
https://gp-docs-cn.github.io/docs/admin_guide/ddl/ddl-partition.html
http://www.postgres.cn/docs/12/ddl.html
原创文章,转载请注明出处:http://www.opcoder.cn/article/58/