likes
comments
collection
share

PostgreSQL技术问答31 - Partitioning 表分区本文讨论了Postgres中,对大型数据表进行逻辑

作者站长头像
站长
· 阅读数 32

本文是《PostgreSQL技术问答》系列文章中的一篇。关于这个系列的由来,可以参阅开篇文章:

文章的编号只是一个标识,在系列中没有明确的逻辑顺序和意义。读者进行阅读时,不用太关注这个方面。

本文讨论的内容是PostgreSQL中的一个重要的容量扩展和性能优化机制:Partitioning,就是表分区。

什么是表分区 Partitioning

表分区(Partitioning),就是将一个比较大的表,在垂直方向上拆分成为多个比较小的表。所谓垂直拆分,就是小表的结构和大表(母表)是一样的,但包含的内容就是记录,都是原来大表其中的一部分。进行表分区操作的理由就是可以针对一些业务应用和场景,通过减少操作可能涉及到的记录范围和规模,来提升操作的性能。所以表分区技术本质上是一种工程手段,目的是改善应用性能。

理论上而言,无论任何数据库系统是否提供原生的表分区功能,我们都可以在应用程序的层面上实现数据表记录的分区存储和处理。就是首先确定一种分区规则和方式,然后在数据插入的时候,基于规则将数据插入到基于分区规则建立的实际的分区数据表中;查询的时候,也基于规则在特定的表中进行查询;最麻烦的是如果数据进行了修改,修改的字段又涉及到分区规则和归属,就需要在不同的分区表中移动这些数据;删除数据相对简单,就在在对应的数据表中找到匹配记录进行删除即可。

可以看到,使用上述方式,在应用系统中实现分区数据的管理,需要对应用程序进行一些改进和调整。但实际上,在一定的条件之下,这些操作是可以抽象出来,在数据库层面上实现的。这样,就出现了数据库系统原生支持的数据分区技术,并作为数据库系统的特性来提供。这样,对于应用程序而言,它对数据的操作就是透明的了,完全不需要修改代码或做什么设置,所有的操作就和原来一样,也能够得到数据分区性能改善的收益。这就是数据库系统提供原生表分区特性的意义和价值。

笔者的记忆中,Oracle系统很早就实现了表分区的功能,也比较完善。而Postgres是在最近几个比较新的系统版本(大概从版本11到版本14)中,逐步实现和完善了表分区的功能,现在也是比较成熟的。

无论何种具体的实现方式,使用数据库分区技术,会带来一些优势,包括:

  • 提高查询性能, 分区裁剪可以减少可能的数据检索规模,或者可以提前优化(分区主键)
  • 易于管理大型数据, 因为可以独立的对分区进行管理,降低了操作大型数据的难度和风险
  • 数据业务分类,有些分区的设计,天然就是按照业务需求进行的,更加贴近业务操作的要求,比如过期数据分类等
  • 简化归档和备份,可以单独的对分区进行归档和备份等操作

当前,其负面影响,就是增加了数据库结构设计的复杂性,提高了对数据管理的要求。规划、使用和操作不当,还会造成数据的冲突和丢失。

实现表分区,需要做哪些处理和工作

作为数据库系统,如果要在系统层面上提供表分区的功能,可能需要考虑以下问题,并提出对应的解决技术和优化方案:

  • 选择和设置分区方式

就是确定以何种方式,确定数据记录应当属于哪个分区。所以其实表分区是可以有很多不同的技术实现方案的,它们也可以适应不同的场景和需求。这个问题我们后面会深入讨论。

  • 优化分区和跨分区的查询

在表分区的结构中,对数据进行跨分区的查询,是一个挑战。如果是单一数据表,这个问题是不存在的。但如果是对数据进行不限制范围的查询,特别是查询结果可能涉及到多个分区的时候,就很容易出问题。当然,如果通过精细的设计和实现,让数据库在查询的时候,能够事先确定查询涉及到特定有限的分区表,肯定会大大提高查询的效率。

  • 数据修改对所在分区的影响

在插入数据的时候,数据库引擎可以根据插入数据的内容,比较简单的分析出来应当将数据插入到哪个分区表当中。删除数据也是类似的情况,最多加上非分区特性查询的分析。但麻烦的是数据修改,如果涉及到可能需要在分区之间进行数据的移动,就不是简单的修改字段那么简单了。分区实现的设计,要重复考虑到这个情况,并且尽可能高效的进行操作。

  • 分区的增加和减少

随着业务的发展或者调整,在实际工作中,可能也会经常遇到原本的数据分区不合理,或者不能满足需求的情况。这时,就需要数据库系统能够提供一个相对合理,操作简便的数据分区调整的机制。例如,Postgres就提出了分区的“挂载(Attach)”和“卸载(Detach)”的操作模式,可以在分区后,切断子分区和主逻辑表的关联关系,或者重新建立关系,这样就给分区的管理提供了很大的方便和灵活。当然,在这其中,需要特别注意分区数据的管理,不能在分区规则上产生逻辑冲突。

  • 操作透明

数据库级别的分区管理的目的,就是希望对于应用系统而言是完全透明的。对应用系统而言,只看到一个主逻辑表;操作也只关联这个表,逻辑上完全无法感知分区的存在或者影响。这需要数据库系统在分区的设计和实现上,尽可能的简化数据管理,或者可靠的实现相关的操作自动化。

如何将数据进行分区

这是表分区技术的重点和核心。是所有的数据库系统,实现表分区首先要考虑的问题。就是针对一条数据记录,决定需要将它存储在哪个分区数据表中。让我们来做一个思想实验。现在我们有一个大的数据表,如何将它拆开,并放在一系列比较小的表中呢?本质上就是一个分类的问题。

首先比较容易想到的。就是为原始数据设计一个“类别”的字段,然后在插入数据的时候,数据必须是其中的一个类别,然后为这些类别设计相同结构的分区表,依据数据的类别将数据插入到对应的表中,其实,这就是所谓的List(列表)方式。

还有一种典型的使用场景,就是数据记录本身有一个时间属性,很多业务需求,也希望使用这个时间属性,将数据进行分类。比如销售的管理就喜欢将销售记录,按照月份对数据进行归类和管理。这时分类的依据,就是月份,但实际上呈现的方式,就是一个时间段,有起止时间,这就是所谓的Range(范围)方式。

还有一种情况,就是找不到任何分类的依据,但数据又太大,就是想要相对平均的将其分成小的部分。这时可以使用数学上的分组方式,就是先按照某种规则,根据数据的特性计算出一个摘要值,然后用余数的方式,将这些其分到一定数量的分区当中,这就是所谓的Hash(散列)方式。这种方式的特点是基于数学而非业务规则进行分区,同时哈希算法(离散性)可以基本保证数据的分组是比较平均的。

综上所述,数据库表分区主要可以使用List(列表)、Range(范围)、Hash(散列)等三种方式,在Postgres中,对于以上三种分区方式,都是支持的。

Postgres如何具体操作和实现表分区

Postgres系统中,对于数据库分区的使用,主要包括创建分区主表、创建分区表和规则、卸载/挂载分区表、数据插入和查询等环节。下面我们使用一个简单的示例代码,来具体研究和分析一下:


-- 创建分区主表,范围方式
CREATE TABLE sales (
    id SERIAL,
    sku varchar(20),
    sale_date DATE,
    amount DECIMAL
) PARTITION BY RANGE (sale_date);

-- 默认分区数据表
CREATE TABLE sales_0000 PARTITION OF sales FOR VALUES DEFAULT;

-- 分区子表
CREATE TABLE sales_2023 PARTITION OF sales
FOR VALUES FROM ('2023-01-01') TO ('2024-01-01');

CREATE TABLE sales_2024 PARTITION OF sales
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');

-- 插入数据
with D(s,a,d) as (values
('Apple', 500, '2023-02-10'),
('Microsoft', 120, '2023-05-10'),
('IBM', 401, '2024-03-20'),
('Nvida', 320, '2024-07-08'),
('AMD', 1200, '2021-07-07'),
('Intel', 80, '2023-11-05')
) insert into sales (sku,amount,sale_date) select s,a,d::date from D;

-- 插入错误分区
defaultdb=> insert into sales_2023(sku, amount, sale_date) values ('Oracle', 830, '2022-02-15');
ERROR:  new row for relation "sales_2023" violates partition constraint
DETAIL:  Failing row contains (22, Oracle, 830, 2022-02-15).

-- 插入分区
defaultdb=> insert into sales_2023(sku, amount, sale_date) values ('Oracle', 830, '2023-02-15');
INSERT 0 1

-- 查询数据
select * from sales;

-- 查询子分区
defaultdb=> select * from sales_0000;
 id | sku | amount | sale_date  
----+-----+--------+------------
 13 | AMD |   1200 | 2021-07-07
(1 row)

-- 跨分区统计查询
defaultdb=> select syear, sum(amount) from (select extract(YEAR from sale_date) syear, amount from sales) S group by 1 order by 2 desc;
 syear | sum  
-------+------
  2021 | 1200
  2024 |  721
  2023 |  700
(3 rows)

-- 更新分区键并查询
defaultdb=> update sales set sale_date = '2023-09-16'  where sku = 'IBM';
UPDATE 1
defaultdb=> select * from sales_2023;
 id |    sku    | amount | sale_date  
----+-----------+--------+------------
  9 | Apple     |    500 | 2023-02-10
 10 | Microsoft |    120 | 2023-05-10
 14 | Intel     |     80 | 2023-11-05
 23 | Oracle    |    830 | 2023-02-15
 11 | IBM       |    401 | 2023-09-16
(5 rows)

上面的操作就是分区表创建和使用的一般方式。这里的要点包括:

  • 分区主表,是在表创建时,使用Partition By关键字指定的
  • 分区主表创建的同时,需要指定分区规则和所使用的字段
  • 分区规则包括Range、List和Hash三种固定类型
  • 分区规则是可以使用多个字段的,称为“复合分区”
  • 然后就可以依据这个分区主表,来创建分区表了
  • 创建分区表,不需要设置数据结构,它们会自动从主表中继承
  • 分区表,使用For Values来指定数据分区的规则,不同的分区类型,有不同的描述语法
  • 对于Range,一般是For Values from ... to ...
  • 使用DEFAULT可以指定默认规则和分区
  • 可以直接在主表中插入数据
  • 如果数据不满足任何规则,则会被插入默认分区
  • 也可以直接插入数据到分区表中,如果不匹配分区条件,则会抛出约束性错误(包括可以插入默认分区的记录)
  • 跨分区查询和统计,可以直接使用主表,和单一数据表无异
  • 如果修改记录的分区规则字段,系统会自动调整其所在分区(包括默认分区)

这里有一个小小的最佳实践的建议,就是为了减少数据冲突或丢失,可以先定义一个默认的分区,所有不满足分区规则的数据,都会插入到这个表分区中。然后我们可以在后期增加分区来进行出来,这时可以保证数据操作不会出现错误。

上面的流程是以Range分区作为示例的,如果是使用List或者Hash,虽然从框架上来看是一样的,但在语法细节上,有一点差别,特别是在分区表定义方面,下面针对这些内容也简单举例说明:


-- 列表数据定义
Create table orders  ... PARTITION BY LIST (status);
CREATE TABLE orders_pending PARTITION OF orders FOR VALUES IN ('Pending');
CREATE TABLE orders_completed PARTITION OF orders FOR VALUES IN ('Completed', 'Cancelled');

-- 散列数据定义, 4个分区
Create table users ... PARTITION BY HASH (userid);

CREATE TABLE users_hash_0 PARTITION OF users FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE users_hash_1 PARTITION OF users FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE users_hash_2 PARTITION OF users FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE users_hash_3 PARTITION OF users FOR VALUES WITH (MODULUS 4, REMAINDER 3);

如何进行表分区的调整

分区表创建完成之后,并不是一成不变的,很多应用和业务的需求,需要对分区表的数据进行调整。比如增加更多的分区,删除分区,合并分区等等。这些操作,本来在严格的分区逻辑和数据约束之下,很容易出现操作的冲突和错误。为此,Postgres提供了attach和detach命令,来简化这些管理工作。

  • 分区挂载 attach

通常情况下,是应该通过指定新的分区规则,直接基于主分区表创建一个新的分区表的。但在一些特殊的情况下,需要将一个原本独立的数据表,加入到主分区表中,这个操作就是挂载。新表的挂载,显然有一些限制,首先就是表结构必须和主分区表一致;然后需要注意数据分区规则是否冲突,通常需要在挂载前进行检查和处理。

下面用一个简单的例子来说明:

-- 创建分区数据冲突
defaultdb=> CREATE TABLE sales_2022 PARTITION OF sales 
FOR VALUES FROM ('2022-01-01') TO ('2023-01-01');
ERROR:  updated partition constraint for default partition "sales_0000" would be violated by some row

-- 创建新的表
defaultdb=> create table sales_2021 (like sales);
CREATE TABLE

defaultdb=> create table sales_2022 as select * from  sales_0000;
SELECT 2

-- 整理数据
defaultdb=> defaultdb=> delete from sales_2022 where sale_date not between '2022-01-01' and '2023-01-01';
DELETE 1^C
defaultdb=> insert into sales_2021 select * from sales_0000 where sale_date between '2021-01-01' and '2022-01-01';
INSERT 0 1

defaultdb=> select * from sales_2021;
 id | sku | amount | sale_date  
----+-----+--------+------------
 13 | AMD |   1200 | 2021-07-07
(1 row)


defaultdb=> delete from sales_2022 where sale_date not between '2022-01-01' and '2023-01-01';
DELETE 1

defaultdb=> delete from sales_0000;
DELETE 2

-- 挂载表
defaultdb=> ALTER TABLE sales
ATTACH PARTITION sales_2021 FOR VALUES FROM ('2021-01-01') TO ('2022-01-01');
ALTER TABLE

defaultdb=>  ALTER TABLE sales
ATTACH PARTITION sales_2022 FOR VALUES FROM ('2022-01-01') TO ('2023-01-01');
ERROR:  column "sku" in child table must be marked NOT NULL

-- 增加约束
alter table sales_2022 alter column sku set not null;
alter table sales_2022 alter column sale_date set not null;

defaultdb=> ALTER TABLE sales                                          
ATTACH PARTITION sales_2022 FOR VALUES FROM ('2022-01-01') TO ('2023-01-01');
ALTER TABLE

这个过程中,可以看出来一些问题。主要就是如果要挂载一个已存在的数据表,这个表的结构、字段数据类型、包括约束等等,必须和主表是一样的。文中使用了create like 方法,(相比create select from 方法)应该可以简化这个操作。

  • 分区卸载 detach

分区卸载的操作相对比较简单,就是将一个表分区,在逻辑上从主分区表中分类出来,形成两个不关联的实体表。这个过程不会影响表中的数据。分离的分区表可以独立工作,但如果查询主表,就看不到这个分区中的内容了。分区卸载通常用于进行分区调整的时候,进行数据操作,减少约束。

还是以一个简单的流程来说明:

-- 插入测试数据
defaultdb=> insert into sales (sku, amount, sale_date) values ('ARM', 78, '2019-05-18' );
INSERT 0 1

-- 确认数据
defaultdb=> select * from sales_0000;
 id | sku | amount | sale_date  
----+-----+--------+------------
 30 | ARM |     78 | 2019-05-18
(1 row)

defaultdb=> select * from sales where sale_date < '2022-1-1';
 id | sku | amount | sale_date  
----+-----+--------+------------
 13 | AMD |   1200 | 2021-07-07
 30 | ARM |     78 | 2019-05-18
(2 rows)

-- 卸载分区
ALTER TABLE sales DETACH PARTITION sales_0000;

-- 确认数据
defaultdb=> select * from sales where sale_date < '2022-1-1';
 id | sku | amount | sale_date  
----+-----+--------+------------
 13 | AMD |   1200 | 2021-07-07
(1 row)

defaultdb=> select * from sales_0000;
 id | sku | amount | sale_date  
----+-----+--------+------------
 30 | ARM |     78 | 2019-05-18
(1 row)

-- 重新挂载
defaultdb=> ALTER TABLE sales ATTACH PARTITION sales_0000 FOR VALUES DEFAULT;
ERROR:  syntax error at or near "DEFAULT"
LINE 1: ... TABLE sales ATTACH PARTITION sales_0000 FOR VALUES DEFAULT;

defaultdb=> ALTER TABLE sales ATTACH PARTITION sales_0000 DEFAULT;
ALTER TABLE

-- 插入检查数据

insert into sales (sku, amount, sale_date) values ('DELL', 227, '2020-08-28' );

defaultdb=> select * from sales where sale_date < '2021-1-1';
 id | sku  | amount | sale_date  
----+------+--------+------------
 30 | ARM  |     78 | 2019-05-18
 31 | DELL |    227 | 2020-08-28
(2 rows)

defaultdb=> select * from sales_0000;
 id | sku  | amount | sale_date  
----+------+--------+------------
 30 | ARM  |     78 | 2019-05-18
 31 | DELL |    227 | 2020-08-28
(2 rows)

这里有一个稍微奇怪一点的设定,就是挂载默认值分区的时候,需要直接使用 default关键字,而非"for values default"(和直接定义不同)。

最后,上面的分区调整方式,只能处理列表和范围类型的分区。对于散列类型的分区,由于在进行分区定义的时候,就已经设置好了数量,应该可以卸载和重新挂载,但不能进行增加或者删除。要调整散列表数量,恐怕要重新创建整个分区体系。

PG的散列分区使用是一个余数,具体是怎么操作的

确实,笔者觉得PG的散列分区定义是比较奇怪的。经过研究,它应该是基于其内置的“hashtext”方法来实现的,而这个函数的底层算法应该是“MurmurHash3”,而不是我们熟悉的md5或者SHA。究其原因,可能是MurmurHash3主要为性能进行了优化设计,它的计算指令相对简单,只使用位操作和乘法运算,从而保证了很高的运算性能,同时又有很好的离散性和低碰撞率。此外,其计算结果是一个32位或者128的整数(SHA是一个字节数组,相比就太复杂了),更适合于后续的处理和使用。所以,它被广泛的应用到数据库、缓存和其他需要快速哈希计算的场合当中。

我们可以执行hashtext函数,来感受一下:

defaultdb=> select hashtext(1001::text), hashtext(1002::text),
hashtext('hello'),hashtext('hellp');

  hashtext   |  hashtext   |  hashtext   |  hashtext  
-------------+-------------+-------------+------------
 -1300200837 | -1185750895 | -1870292951 | -369689658
(1 row)

所以,即使分区规则基于一个整数的字段,postgres也会将其先转为字符串来进行出来,这样可以保证结果的离散性,这就可以很好的解释了,postgres的散列分区,并没有严格要求分区字段值的数据类型,而是统一的使用了整除余数作为分区依据,简单而高效。

但笔者觉得,它的实现方式有点麻烦,需要在分区表中定义除数和余数,完全可以在主分区表中定义除数,然后在分区表中定义分区对应的余数。这样可以减少由于拼写造成的定义和数据错误。

PG当前实现的表分区,包括那些特性

这些信息,可以从Postgres的Feature Matrix(功能特性矩阵)更清晰的看到。分区的相关特性,在其中是属于“Partitioning & Inheritance,分区和继承”这个板块。 具体的条目包括:

  • Accelerated partition pruning 加速的分区修剪
  • Declarative table partitioning 声明表分区,分区相关定义和指令
  • Default Partition 默认分区
  • Foreign Key references for partitioned tables 分区表作为外键参考
  • Foreign table inheritance 外部表继承
  • Partitioning by a hash key 使用散列键分区
  • Partition pruning during query execution 查询执行时分区修剪
  • Support for PRIMARY KEY, FOREIGN KEY, indexes, and triggers on partitioned tables 分区表对主键、外键、索引和触发器的支持
  • Table Partitioning 分区表,基础实现
  • UPDATE on a partition key 分区键值更新时,重新调整记录所在分区

什么是“Pruning”,分区裁剪

这个技术名词(Pruning,裁剪)的曝光率并不高,但其实这才是数据库分区技术的核心。在分区表的结构体系中,系统可以在相关执行的查询规划节点,通过动态的分析查询条件,来确定哪些分区可能包括满足条件的行;然后将这些信息传输给查询执行器,它们可以只处理选定的分区,忽略(裁剪)掉其余分区,从而减少了I/O和处理开销。基于系统基本的优化和实现,Postgres的分区裁剪执行各种查询类型和各种分区方式,具备广泛的适用性。

如对于Select、Update和Delete语句,如果WHERE子句中存在限制分区选择的条件,则只访问选定的分区。而对于 Insert语句,如果目标表是分区主表,并且存在限制分区选择的条件,则只插入到选定的分区表中。

当然,和所有优化技术一样,分区裁剪的应用也有一些局限。包括:

  • 分区修剪不适用于某些类型的查询,如涉及全表扫描或不包含限制条件的查询
  • 为了使分区修剪有效,查询条件应与分区键相关
  • 分区修剪可能不会在涉及多个表的复杂查询中提供最佳性能。

另外,在Postgres中,分区裁剪还是一个系统基本的设置项目,可以手动的关闭和开启,当然,默认情况下这个选项时开启的。下面这些查询分析的案例,可以帮助我们了解分区裁剪的开关,对于查询计划和执行的影响:

-- 关闭裁剪 
SET enable_partition_pruning = off;
EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01';
                                    QUERY PLAN
-------------------------------------------------------------------​----------------
 Aggregate  (cost=188.76..188.77 rows=1 width=8)
   ->  Append  (cost=0.00..181.05 rows=3085 width=0)
         ->  Seq Scan on measurement_y2006m02  (cost=0.00..33.12 rows=617 width=0)
               Filter: (logdate >= '2008-01-01'::date)
         ->  Seq Scan on measurement_y2006m03  (cost=0.00..33.12 rows=617 width=0)
               Filter: (logdate >= '2008-01-01'::date)
...
         ->  Seq Scan on measurement_y2007m11  (cost=0.00..33.12 rows=617 width=0)
               Filter: (logdate >= '2008-01-01'::date)
         ->  Seq Scan on measurement_y2007m12  (cost=0.00..33.12 rows=617 width=0)
               Filter: (logdate >= '2008-01-01'::date)
         ->  Seq Scan on measurement_y2008m01  (cost=0.00..33.12 rows=617 width=0)
               Filter: (logdate >= '2008-01-01'::date)
               

-- 启用裁剪
SET enable_partition_pruning = on;
EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01';
                                    QUERY PLAN
-------------------------------------------------------------------​----------------
 Aggregate  (cost=37.75..37.76 rows=1 width=8)
   ->  Seq Scan on measurement_y2008m01  (cost=0.00..33.12 rows=617 width=0)
         Filter: (logdate >= '2008-01-01'::date)         

可以看到,即使没有使用索引对查询进行优化,但将查询能够限制在有限的分区表中,取得的优化效果,也是很显著的。

关于表分区,有什么最佳实践的建议

确实,Postgres官方文档,有专门的章节讨论这个问题。但原文完全是一种叙述性的模式,没有太多的结构和组织。笔者用cohere进行一个简单的总结和组织,并添加了一些自己的观点和思考。大致内容如下:

  • 优先使用声明式分区(Declarative Partitioning)

早先的Postgres系统并没有专门的分区功能。它在工程上的实现,是基于继承表来对表结构进行管理,并使用触发器来实现数据的自动分区操作。当然在比较新的版本中,已经实现了现在这种声明式的分区管理,就是可以在语法中,增加了分区的声明和定义,并在数据库系统级别,实现了分区的相关数据操作和管理。

  • 合理使用 CHECK 约束

建议在每个分区上使用CHECK约束来定义分区约束。这提供了清晰的分区定义,并确保数据完整性。并且避免在CHECK 约束中使用分区键以外的列。这可以简化分区定义,并避免潜在的性能问题。使用分区键以外的列可能会导致性能下降,因为它需要额外的计算来评估约束。

  • 分区键的选择

选择适当的分区键对于声明性分区至关重要。建议选择具有良好基数和分布的列,以确保均匀的分区。分区键应反映查询模式,使查询能够有效利用分区修剪。

  • 关于分区数量

文章建议根据预期的数据量和查询模式选择适当数量的分区。分区数量应根据数据增长和查询需求进行调整。但过多的分区可能会导致性能下降。

  • 关于分区边界

对于范围分区,应仔细选择分区边界以确保均匀的数据分布。建议使用可扩展的边界值,以适应未来数据的增长。

  • 分区维护

文中强调了定期维护分区表的重要性,包括重新组织分区、分析和清理。并且建议使用自动化工具和脚本来简化维护任务。

这些最佳实践和建议,应该能够帮助数据库管理员有效地使用PostgreSQL提供的分区功能,以在总体上提高系统和数据管理的性能和可维护性。

表分区可能会有什么问题和限制

在Postgres的官方技术文档中,描述了一些表分区技术的限制:

  • 在分区表上创建唯一约束或主键约束时,分区键不能包含任何表达式或函数调用,并且约束的列必须包含所有的分区键列。这个限制是因为组成约束的各个索引只能直接在其各自的分区内强制唯一性;因此,分区结构本身必须确保不同分区之间没有重复项
  • 无法在整个分区表上创建排除约束(只能在分区表内单独设置)
  • INSERT操作中的BEFORE ROW触发器不能改变新行最终被插入到哪个分区
  • 在同一分区树中不能混合使用临时和永久关系,如果分区主表是永久的,其分区也必须是永久的,反之亦然
  • 如果使用临时关系,分区树中的所有成员必须来自同一个会话
  • 各个分区在后台通过继承与其分区表关联。然而,声明式分区表及其分区无法使用继承的所有通用功能,具体如下。特别是,分区不能有除分区表之外的其他父表,也不能既继承自分区表又继承自普通表。这意味着分区表及其分区从未与普通表共享继承层次结构。

这个技术文档,还透露出一些分区实现的方式和细节。在Postgres中,分区表和其分区是一个继承层次结构,tableoid和所有普通的继承规仍然适用。但分区体系有一些例外:

  • 分区不能有父表中不存在的列,创建分区时不能指定所使用的列(在主表中已经定义),也不能事后向分区添加列
  • 挂载分区时,子表的列与父表需要完全匹配时
  • 分区表的CHECK 和 NOT NULL约束始终被所有分区继承,而且不能在分区中删除
  • 只要分区表没有分区,支持使用ONLY来仅对分区表添加或删除约束
  • 一旦存在分区,除UNIQUE和PRIMARY KEY 之外,使用 ONLY 会导致错误。相反,约束可以直接添加到分区上,并且(如果它们不存在于父表中)可以删除。
  • 由于分区表本身没有任何数据,尝试仅对分区表使用TRUNCATE ONLY总是会返回错误

另外,根据笔者测试和使用的一些经验,这里总结了一下使用分区表时,需要注意的问题:

  • 创建和调整分区时,可能和现有的数据冲突,需要一些前期的数据整理
  • 创建和调整分区,需要仔细检查数据表结构,特别是约束关系,确保逻辑和业务合规
  • 不支持复合分区规则,当然也不建议,那样会将数据管理复杂化
  • 不支持嵌套分区,绝大多数情况下也没有这个必要

其实关于分区,和分区的操作,还有很多细节的技术问题,如分区的约束、触发器、索引等等,这里由于篇幅和时间限制,只能讨论核心的概念和流程,其他的内容无法一一展开研究和讨论。这些知识和信息,都需要在日常的工作和使用中,逐渐丰富和完善。

小结

本文讨论了Postgres中,对大型数据表进行逻辑分区的处理技术-Partitioning。这个技术通过将数据进行垂直维度的分隔,可以在一些场景中减少数据操作可能涉及的规模,从而提高处理效率。文中探讨了其基本原理、分区的类型和选择方式、具体操作和流程、分区的管理等方面的内容。

转载自:https://juejin.cn/post/7412506389334245430
评论
请登录