PostgreSQL-表分区

分区是指将逻辑上是一张大表拆分为较小的物理块。分区可以提供几个好处:

  1. 在某些情况下,查询性能可以显着提高,尤其是当表的大部分访问量很大的行位于单个分区或少数几个分区中时。分区有效地替代了较高的树级别索引,使得索引的大量使用部分更有可能适合内存。

  2. 当查询或更新访问单个分区的大部分时,可以通过使用该分区的顺序扫描而不是使用索引来提高性能,这需要分散在整个表中的随机访问读取。

  3. 如果在分区设计中考虑了使用模式,则可以通过添加或删除分区来完成批量加载和删除。使用 DROP TABLE 或执行 ALTER TABLE DETACH PARTITION 删除单个分区比批量操作快得多。这些命令还完全避免了由批量 DELETE 引起的 VACUUM 开销。

  4. 很少使用的数据可以迁移到更便宜、更慢的存储介质上。

PostgreSQL 为以下分区形式提供内置支持:

Range Partitioning
该表被划分为由一个键列或一组列定义的“范围”,分配给不同分区的值范围之间没有重叠。例如,可以按日期范围或特定业务对象的标识符范围进行分区。每个范围的界限被理解为在下端包含在内,在上端不包含在内。例如,如果一个分区的范围是从 1 到 10,而下一个分区的范围是从 10 到 20,那么值 10 属于第二个分区而不是第一个。

List Partitioning
通过显式列出每个分区中出现的键值对表进行分区。

Hash Partitioning
通过为每个分区指定模数和余数来对表进行分区。每个分区将保存分区键的哈希值除以指定模数将产生指定余数的行。

一、声明式分区

PostgreSQL 允许您声明一个表被划分为多个分区。被划分的表称为分区表。该声明包括如上所述的分区方法,以及用作分区键的列或表达式的列表。

分区表本身是一个“虚拟”表,没有自己的存储空间。相反,存储属于分区,这些分区是与分区表关联的普通表。每个分区存储由其分区边界定义的数据子集。插入分区表的所有行都将根据分区键列的值路由到适当的分区之一。如果行的分区键不再满足其原始分区的分区边界,则更新行的分区键将导致它被移动到不同的分区中。

分区本身可以定义为分区表,从而产生子分区。尽管所有分区都必须具有与其分区父级相同的列,但分区可能有自己的索引、约束和默认值,与其他分区不同。

无法将常规表转换为分区表,反之亦然。但是,可以将现有的常规表或分区表添加为分区表的分区,或者从分区表中删除分区将其变成独立表;这可以简化和加快许多维护过程。

分区也可以是外部表,尽管需要非常小心,因为外部表的内容满足分区规则是用户的责任。还有一些其他限制。

举例:

假设我们正在为一家大型冰淇淋公司构建一个数据库。该公司每天测量峰值温度以及每个地区的冰淇淋销售情况。从概念上讲,我们想要一个像这样的表:

CREATE TABLE measurement (
    city_id         int not null,
    logdate         date not null,
    peaktemp        int,
    unitsales       int
);

我们知道,大多数查询只会访问上周、月份或季度的数据,因为此表的主要用途是准备在线报告以供管理。为了减少需要存储的旧数据量,我们决定只保留最近 3 年的数据。在每个月初,我们将删除最早月份的数据。在这种情况下,我们可以使用分区来帮助我们满足对测量表的所有不同要求。

要在这种情况下使用声明性分区,请使用以下步骤:

1.通过指定 PARTITION BY 子句将measurement表创建为分区表,其中包括分区方法(在本例中为 RANGE)和用分区键。

CREATE TABLE measurement (
    city_id         int not null,
    logdate         date not null,
    peaktemp        int,
    unitsales       int
) PARTITION BY RANGE (logdate);

2.创建分区。每个分区的定义必须指定与父分区方法和分区键相对应的边界。请注意,指定边界以使新分区的值与一个或多个现有分区中的值重叠会导致错误。

这样创建的分区在各个方面都是普通的 PostgreSQL 表(或者,可能是外部表)。可以分别为每个分区指定表空间和存储参数。

对于我们的示例,每个分区应保存一个月的数据,以匹配一次删除一个月数据的要求。所以命令可能看起来像:

create table measurement_2022_1 partition of measurement
for values from ('2022-01-01') to ('2022-02-01');

 create table measurement_2022_2 partition of measurement
for values from ('2022-02-01') to ('2022-03-01');

 create table measurement_2022_3 partition of measurement
 for values from ('2022-03-01') to ('2022-04-01');

如果您希望实现子分区,请在用于创建单个分区的命令中再次指定 PARTITION BY 子句,例如:

CREATE TABLE measurement_y2006m02 PARTITION OF measurement
FOR VALUES FROM ('2006-02-01') TO ('2006-03-01')
PARTITION BY RANGE (peaktemp);

在创建measurement_y2006m02的分区后,任何插入到measurement_y2006m02的数据(或直接插入到measurement_y2006m02的数据,只要满足其分区约束就可以)将进一步重定向到基于peaktemp列的分区之一.指定的分区键可能与父分区键重叠,但在指定子分区的边界时应小心,使其接受的数据集构成分区自身边界允许的数据集;系统不会尝试检查是否确实如此。

将数据插入到未映射到现有分区之一的父表中会导致错误;必须手动添加适当的分区。

不必手动创建描述分区的分区边界条件的表约束。此类约束将自动创建。

3.在分区表上的键列以及您可能需要的任何其他索引上创建索引。(键索引不是绝对必要的,但在大多数情况下它是有帮助的。)这会自动在每个分区上创建一个匹配的索引,并且您以后创建或附加的任何分区也将具有这样的索引。在分区表上声明的索引或唯一约束是“虚拟的”,就像分区表一样:实际数据位于各个分区表的子索引中。

CREATE INDEX ON measurement (logdate);

4.确保在 postgresql.conf 中未禁用 enable_partition_pruning 配置参数。如果是,查询将不会按需要进行优化。

测试:

insert into measurement(city_id,logdate,peaktemp,unitsales)  values(1,'2022-01-01',1,1);
insert into measurement(city_id,logdate,peaktemp,unitsales)  values(2,'2022-02-01',2,2);
insert into measurement(city_id,logdate,peaktemp,unitsales)  values(3,'2022-03-01',3,3);

通常,最初定义表时建立的分区集并不打算保持静态。通常希望删除保存旧数据的分区并定期为新数据添加新分区。分区最重要的优点之一正是它允许通过操纵分区结构几乎立即执行这个原本很痛苦的任务,而不是物理移动大量数据。

删除旧数据的最简单选择是删除不再需要的分区:

DROP TABLE measurement_y2006m02;

这可以非常快速地删除数百万条记录,因为它不必单独删除每条记录。但是请注意,上述命令需要在父表上获取 ACCESS EXCLUSIVE 锁。

通常更可取的另一个选项是从分区表中删除分区,但保留对它的访问权限作为一个单独的表。这有两种形式:

ALTER TABLE measurement DETACH PARTITION measurement_y2006m02;
ALTER TABLE measurement DETACH PARTITION measurement_y2006m02 CONCURRENTLY;

这些允许在删除数据之前对数据执行进一步的操作。例如,这通常是使用 COPY、pg_dump 或类似工具备份数据的有用时间。这也可能是将数据聚合成更小的格式、执行其他数据操作或运行报告的有用时间。该命令的第一种形式需要父表上的 ACCESS EXCLUSIVE 锁。在第二种形式中添加 CONCURRENTLY 限定符允许分离操作只需要父表上的 SHARE UPDATE EXCLUSIVE 锁.

同样,我们可以添加一个新分区来处理新数据。我们可以在分区表中创建一个空分区,就像上面创建原始分区一样:

CREATE TABLE measurement_y2008m02 PARTITION OF measurement
FOR VALUES FROM ('2008-02-01') TO ('2008-03-01')
TABLESPACE fasttablespace;

作为替代方案,有时更方便的是在分区结构之外创建新表,并在以后使其成为适当的分区。这允许在新数据出现在分区表中之前对其进行加载、检查和转换。CREATE TABLE ... LIKE 选项有助于避免繁琐地重复父表的定义:

CREATE TABLE measurement_y2008m02
(LIKE measurement INCLUDING DEFAULTS INCLUDING CONSTRAINTS)
TABLESPACE fasttablespace;

ALTER TABLE measurement_y2008m02 ADD CONSTRAINT y2008m02
CHECK ( logdate >= DATE '2008-02-01' AND logdate < DATE '2008-03-01' );

ALTER TABLE measurement ATTACH PARTITION measurement_y2008m02
FOR VALUES FROM ('2008-02-01') TO ('2008-03-01' );

ATTACH PARTITION 命令需要对分区表使用 SHARE UPDATE EXCLUSIVE 锁。

在运行 ATTACH PARTITION 命令之前,建议在要附加的表上创建一个与预期分区约束匹配的 CHECK 约束,如上所示。这样,系统将能够跳过验证隐式分区约束所需的扫描。如果没有 CHECK 约束,将扫描表以验证分区约束,同时在该分区上持有 ACCESS EXCLUSIVE 锁。建议在 ATTACH PARTITION 完成后删除现在冗余的 CHECK 约束。如果被附加的表本身是一个分区表,那么它的每个子分区将被递归锁定和扫描,直到遇到合适的 CHECK 约束或到达叶分区。

同样,如果分区表有DEFAULT分区,建议创建一个CHECK约束,排除待附加分区的约束。如果不这样做,则将扫描 DEFAULT 分区以验证它不包含应该位于所附加分区中的记录。此操作将在 DEFAULT 分区上持有 ACCESS EXCLUSIVE 锁时执行。如果 DEFAULT 分区本身是一个分区表,那么它的每个分区都将以与附加表相同的方式进行递归检查,如上所述。

如上所述,可以在分区表上创建索引,以便将它们自动应用于整个层次结构。这非常方便,因为不仅现有的分区会被索引,而且将来创建的任何分区也会被索引。一个限制是在创建这样的分区索引时不能使用 CONCURRENTLY 限定符。为了避免长时间的锁定,可以只在分区表上使用 CREATE INDEX;这样的索引被标记为无效,并且分区不会自动应用索引。可以使用 CONCURRENTLY 单独创建分区上的索引,然后使用 ALTER INDEX .. ATTACH PARTITION 将其附加到父级上的索引。一旦所有分区的索引都附加到父索引,父索引就会自动标记为有效。例子:

CREATE INDEX measurement_usls_idx ON ONLY measurement (unitsales);


CREATE INDEX measurement_usls_200602_idx
ON measurement_y2006m02 (unitsales);

ALTER INDEX measurement_usls_idx
ATTACH PARTITION measurement_usls_200602_idx;

这种技术也可以与 UNIQUE 和 PRIMARY KEY 约束一起使用;索引是在创建约束时隐式创建的。例子:
ALTER TABLE ONLY measurement ADD UNIQUE (city_id, logdate);

ALTER TABLE measurement_y2006m02 ADD UNIQUE (city_id, logdate);

ALTER INDEX measurement_city_id_logdate_key
ATTACH PARTITION measurement_y2006m02_city_id_logdate_key;

限制:

以下限制适用于分区表:

1.分区表上的唯一约束(以及主键)必须包括所有分区键列。存在此限制是因为构成约束的各个索引只能在其自己的分区内直接强制唯一性;因此,分区结构本身必须保证不同分区不存在重复。

2.无法创建跨越整个分区表的排除约束。只能对每个叶分区单独设置这样的约束。同样,此限制源于无法强制执行跨分区限制。

3.INSERT 上的 BEFORE ROW 触发器无法更改哪个分区是新行的最终目标。

4.不允许在同一分区树中混合临时和永久表。因此,如果分区表是永久的,那么它的分区也必须是永久的,如果分区表是临时的,也是如此。使用临时关系时,分区树的所有成员必须来自同一个会话。

各个分区使用幕后的继承链接到它们的分区表。但是,不可能将继承的所有通用特性与声明式分区表或其分区一起使用,如下所述。值得注意的是,除了作为分区的分区表之外,分区不能有任何父级,表也不能从分区表和常规表继承。这意味着分区表及其分区永远不会与常规表共享继承层次结构。

由于由分区表及其分区组成的分区层次结构仍然是继承层次结构,tableoid 和所有正常的继承规则都适用,,但有一些例外:

1.分区不能有父级中不存在的列。使用 CREATE TABLE 创建分区时无法指定列,也无法使用 ALTER TABLE 事后将列添加到分区。仅当表的列与父表完全匹配时,才可以使用 ALTER TABLE ... ATTACH PARTITION 将表添加为分区。

2.分区表的 CHECK 和 NOT NULL 约束始终由其所有分区继承。不允许在分区表上创建标记为 NO INHERIT 的 CHECK 约束。如果父表中存在相同的约束,则不能在分区的列上删除 NOT NULL 约束。

3.只要没有分区,就支持使用 ONLY 仅在分区表上添加或删除约束。一旦分区存在,使用 ONLY 将导致错误。相反,可以添加和删除分区本身的约束(如果它们不存在于父表中)。

4.由于分区表本身没有任何数据,因此尝试在分区表上使用 TRUNCATE ONLY 将始终返回错误。

二、使用继承进行分区

虽然内置的声明性分区适用于最常见的用例,但在某些情况下,更灵活的方法可能有用。分区可以使用表继承来实现,它允许声明性分区不支持的几个特性,例如:

1.对于声明性分区,分区必须具有与分区表完全相同的列集,而对于表继承,​​子表可能具有父表中不存在的额外列。

2.表继承允许多重继承。

3.声明式分区仅支持范围、列表和散列分区,而表继承允许以用户选择的方式划分数据。(但是请注意,如果约束排除无法有效地修剪子表,则查询性能可能会很差。)

举例:

此示例构建了一个与上面的声明性分区示例等效的分区结构。使用以下步骤:

1.创建“根”表,所有“子”表都将从该表继承。该表将不包含任何数据。不要在此表上定义任何检查约束,除非您打算将它们平等地应用于所有子表。在其上定义任何索引或唯一约束也没有意义。对于我们的示例,根表是最初定义的measurement表:

CREATE TABLE measurement (
    city_id         int not null,
    logdate         date not null,
    peaktemp        int,
    unitsales       int
);

2.创建几个从根表继承的“子”表。通常,这些表不会将任何列添加到从根继承的集合中。就像声明式分区一样,这些表在任何方面都是普通的 PostgreSQL 表(或外部表)。

CREATE TABLE measurement_2022_2 () INHERITS (measurement);
CREATE TABLE measurement_2022_3 () INHERITS (measurement);
CREATE TABLE measurement_2022_4 () INHERITS (measurement);

3.将非重叠表约束添加到子表以定义每个子表中允许的键值。
典型的例子是:

CHECK ( x = 1 )
CHECK ( county IN ( 'Oxfordshire', 'Buckinghamshire', 'Warwickshire' ))
CHECK ( outletID >= 100 AND outletID < 200 )

确保约束保证不同子表中允许的键值之间没有重叠。一个常见的错误是设置范围约束,例如:

CHECK ( outletID BETWEEN 100 AND 200 )
CHECK ( outletID BETWEEN 200 AND 300 )

为子表添加约束:

alter table measurement_2022_2 add check(logdate >= date '2022-02-01' and logdate < date '2022-03-01');

alter table measurement_2022_3 add check(logdate >= date '2022-03-01' and logdate < date '2022-04-01');

alter table measurement_2022_4 add check(logdate >= date '2022-04-01' and logdate < date '2022-05-01');

4.对于每个子表,在键列以及您可能需要的任何其他索引上创建一个索引。

 create index measurement_2022_2_logdate on measurement_2022_2(logdate);

 create index measurement_2022_3_logdate on measurement_2022_3(logdate);

 create index measurement_2022_4_logdate on measurement_2022_4(logdate);

5.我们希望我们的应用程序能够说 INSERT INTO measure ... 并将数据重定向到适当的子表中。我们可以通过将合适的触发器函数附加到根表来安排。如果数据只会添加到最新的孩子,我们可以使用一个非常简单的触发函数:

CREATE OR REPLACE FUNCTION measurement_insert_trigger()
RETURNS TRIGGER AS $$
BEGIN
    IF ( NEW.logdate >= DATE '2022-02-01' AND
         NEW.logdate < DATE '2022-03-01' ) THEN
        INSERT INTO measurement_2022_2 VALUES (NEW.*);
    ELSIF ( NEW.logdate >= DATE '2022-03-01' AND
            NEW.logdate < DATE '2022-04-01' ) THEN
        INSERT INTO measurement_2022_3 VALUES (NEW.*);
    ELSIF ( NEW.logdate >= DATE '2022-04-01' AND
            NEW.logdate < DATE '2022-05-01' ) THEN
        INSERT INTO measurement_2022_4 VALUES (NEW.*);
    ELSE
        RAISE EXCEPTION 'Date out of range.  Fix the measurement_insert_trigger() function!';
    END IF;
    RETURN NULL;
END;
$$
LANGUAGE plpgsql;

创建函数后,我们创建一个调用触发器函数的触发器:

CREATE TRIGGER insert_measurement_trigger
BEFORE INSERT ON measurement
FOR EACH ROW EXECUTE FUNCTION measurement_insert_trigger();

将插入重定向到适当的子表的另一种方法是在根表上设置规则而不是触发器。例如:

CREATE RULE measurement_insert_y2006m02 AS
ON INSERT TO measurement WHERE
    ( logdate >= DATE '2006-02-01' AND logdate < DATE '2006-03-01' )
DO INSTEAD
    INSERT INTO measurement_y2006m02 VALUES (NEW.*);

规则的开销比触发器多得多,但开销是每个查询支付一次,而不是每行一次,因此这种方法可能对批量插入情况有利。然而,在大多数情况下,触发方法会提供更好的性能。

请注意 COPY 忽略规则。如果要使用 COPY 插入数据,则需要复制到正确的子表中,而不是直接复制到根表中。COPY 确实会触发触发器,因此如果您使用触发器方法,则可以正常使用它。

规则方法的另一个缺点是,如果规则集不涵盖插入日期,则没有简单的方法来强制出错。数据将静默地进入根表。

6.确保 postgresql.conf 中没有禁用 constraint_exclusion 配置参数;否则可能会不必要地访问子表。

测试:

insert into measurement(city_id,logdate,peaktemp,unitsales)  values(2,'2022-02-01',2,2);

insert into measurement(city_id,logdate,peaktemp,unitsales)  values(3,'2022-03-01',3,3);

insert into measurement(city_id,logdate,peaktemp,unitsales)  values(4,'2022-04-01',4,4);

要快速删除旧数据,只需删除不再需要的子表:

DROP TABLE measurement_y2006m02;

要从继承层次结构表中删除子表,但保留对它的访问权限作为一个单独的表:

ALTER TABLE measurement_y2006m02 NO INHERIT measurement;

要添加新的子表来处理新数据,请创建一个空子表,就像上面创建的原始子表一样:

CREATE TABLE measurement_y2008m02 (
    CHECK ( logdate >= DATE '2008-02-01' AND logdate < DATE '2008-03-01' )
) INHERITS (measurement);

或者,可能希望在将新子表添加到表层次结构之前创建并填充它。这可以允许在父表上的查询可见之前加载、检查和转换数据。

CREATE TABLE measurement_y2008m02
  (LIKE measurement INCLUDING DEFAULTS INCLUDING CONSTRAINTS);
ALTER TABLE measurement_y2008m02 ADD CONSTRAINT y2008m02
   CHECK ( logdate >= DATE '2008-02-01' AND logdate < DATE '2008-03-01' );
\copy measurement_y2008m02 from 'measurement_y2008m02'
-- possibly some other data preparation work
ALTER TABLE measurement_y2008m02 INHERIT measurement;

注意事项:

1.没有自动方法来验证所有 CHECK 约束是否互斥。创建生成子表并创建和/或修改关联对象的代码比手动编写每个代码更安全。

2.索引和外键约束适用于单个表,而不适用于它们的继承子表,因此需要注意一些警告。

3.此处显示的方案假定行的键列的值永远不会改变,或者至少不会改变到足以要求它移动到另一个分区的程度。由于 CHECK 约束,尝试执行此操作的 UPDATE 将失败。如果你需要处理这种情况,你可以在子表上放置合适的更新触发器,但这会使结构的管理变得更加复杂。

4.如果您使用手动 VACUUM 或 ANALYZE 命令,请不要忘记您需要在每个子表上单独运行它们。像这样的命令:只会处理根表。

ANALYZE measurement;

5.带有 ON CONFLICT 子句的 INSERT 语句不太可能按预期工作,因为 ON CONFLICT 操作仅在指定目标关系(而不是其子关系)存在唯一违规的情况下才会采取。

6.除非应用程序明确知道分区方案,否则将需要触发器或规则将行路由到所需的子表。触发器编写起来可能很复杂,并且比通过声明性分区在内部执行的元组路由要慢得多。

三、分区修剪
分区修剪是一种查询优化技术,可提高声明式分区表的性能。举个例子:

SET enable_partition_pruning = on; 
SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01';

如果没有分区修剪,上述查询将扫描measurement表的每个分区。启用分区修剪后,计划程序将检查每个分区的定义并证明不需要扫描分区,因为它不能包含任何满足查询的 WHERE 子句的行。当计划者可以证明这一点时,它会从查询计划中排除(修剪)该分区。

通过使用 EXPLAIN 命令和 enable_partition_pruning 配置参数,可以显示已修剪分区的计划与未修剪分区的计划之间的差异。这种类型的表设置的典型未优化计划是:

SET enable_partition_pruning = off;
EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2022-02-01';

部分或全部分区可能使用索引扫描而不是全表顺序扫描,但这里的重点是根本不需要扫描旧分区来回答这个查询。当我们启用分区修剪时,我们会得到一个便宜得多的计划,它会提供相同的答案:

SET enable_partition_pruning = on;
EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2022-02-01';

请注意,分区修剪仅由分区键隐式定义的约束驱动,而不是由索引的存在驱动。因此没有必要在键列上定义索引。是否需要为给定分区创建索引取决于您是否希望扫描分区的查询通常会扫描分区的大部分或仅扫描一小部分。索引在后一种情况下会有所帮助,但对前者没有帮助。

分区修剪不仅可以在给定查询的计划期间执行,也可以在其执行期间执行。这很有用,因为当子句包含在查询计划时其值未知的表达式时,它可以允许修剪更多分区,例如,在 PREPARE 语句中定义的参数,使用从子查询获得的值,或使用参数化的值嵌套循环连接的内侧。执行期间的分区修剪可以在以下任何时间执行:

1.在查询计划初始化期间。对于在执行的初始化阶段已知的参数值,可以在此处执行分区修剪。在此阶段修剪的分区不会出现在查询的 EXPLAIN 或 EXPLAIN ANALYZE 中。通过观察 EXPLAIN 输出中的“Subplans Removed”属性,可以确定在此阶段删除的分区数。

2.在查询计划的实际执行期间。也可以在这里执行分区修剪以使用仅在实际查询执行期间才知道的值来删除分区。这包括来自子查询的值和来自执行时参数的值,例如来自参数化嵌套循环连接的值。由于这些参数的值在查询执行期间可能会多次更改,因此只要分区修剪使用的执行参数之一发生更改,就会执行分区修剪。要确定在此阶段是否修剪了分区,需要仔细检查 EXPLAIN ANALYZE 输出中的 loops 属性。与不同分区对应的子计划可能具有不同的值,具体取决于它们在执行期间被修剪的次数。如果每次都被修剪,有些可能会显示为(从未执行)。

四、分区和约束排除

约束排除是一种类似于分区修剪的查询优化技术。虽然它主要用于使用遗留继承方法实现的分区,但它可以用于其他目的,包括声明性分区。

约束排除的工作方式与分区修剪非常相似,不同之处在于它使用每个表的 CHECK 约束——这就是它的名字——而分区修剪使用表的分区边界,它只存在于声明性分区的情况下。另一个区别是约束排除仅在计划时应用;在执行时没有尝试删除分区。

约束排除使用 CHECK 约束这一事实,这使得它比分区修剪慢,有时可以用作优势:因为约束甚至可以在声明性分区表上定义,除了它们的内部分区边界之外,约束排除可能是能够从查询计划中删除额外的分区。

constraint_exclusion 的默认(和推荐)设置既不是 on 也不是 off,而是一个称为 partition 的中间设置,这导致该技术仅适用于可能在继承分区表上工作的查询。on 设置使计划程序检查所有查询中的 CHECK 约束,即使是不太可能受益的简单查询。

以下注意事项适用于约束排除:

1.约束排除仅在查询计划期间应用,与分区修剪不同,分区修剪也可以在查询执行期间应用。

2.约束排除仅在查询的 WHERE 子句包含常量(或外部提供的参数)时才有效。例如,不能优化与诸如 CURRENT_TIMESTAMP 等非不可变函数的比较,因为规划器无法知道函数的值在运行时可能落入哪个子表。

3.保持分区约束简单,否则规划器可能无法证明可能不需要访问子表。对列表分区使用简单的相等条件,或者对范围分区使用简单的范围测试。

4.在约束排除期间检查父表的所有子表的所有约束,因此大量子表可能会显着增加查询计划时间。因此,基于传统继承的分区可以很好地处理多达一百个子表;不要试图使用成千上万的孩子。

五、声明式分区的最佳实践

应该谨慎选择如何对表进行分区,因为糟糕的设计可能会对查询计划和执行的性能产生负面影响。

最关键的设计决策之一将是您对数据进行分区的一个或多个列。通常最好的选择是按最常出现在分区表上执行的查询的 WHERE 子句中的列或列集进行分区。与分区绑定约束兼容的 WHERE 子句可用于修剪不需要的分区。但是,您可能会因 PRIMARY KEY 或 UNIQUE 约束的要求而被迫做出其他决定。删除不需要的数据也是规划分区策略时要考虑的一个因素。整个分区可以相当快地分离,因此以这样一种方式设计分区策略可能是有益的,即一次要删除的所有数据都位于单个分区中。

选择表应该划分的目标分区数也是一个关键的决定。没有足够的分区可能意味着索引仍然太大并且数据局部性仍然很差,这可能导致缓存命中率低。但是,将表划分为太多分区也会导致问题。太多的分区可能意味着更长的查询计划时间和查询计划和执行期间的更高内存消耗,如下所述。在选择如何对表进行分区时,考虑未来可能发生的变化也很重要。例如,如果您选择为每个客户设置一个分区,而您目前只有少量大客户,请考虑如果几年后您发现自己拥有大量小客户的影响。在这种情况下,最好选择按 HASH 分区并选择合理数量的分区,而不是尝试按 LIST 分区并希望客户数量不会增加超出数据分区的实际范围。

子分区对于进一步划分预期会比其他分区更大的分区很有用。另一种选择是对分区键中的多个列使用范围分区。其中任何一个都容易导致分区数量过多,因此建议克制。

在查询计划和执行期间考虑分区的开销很重要。查询规划器通常能够很好地处理多达几千个分区的分区层次结构,前提是典型的查询允许查询规划器修剪除少量分区之外的所有分区。当规划器执行分区修剪后剩余的分区更多时,规划时间变得更长,内存消耗变得更高。担心有大量分区的另一个原因是服务器的内存消耗可能会随着时间的推移而显着增加,尤其是当许多会话涉及大量分区时。这是因为每个分区都需要将其元数据加载到每个与其接触的会话的本地内存中。

对于数据仓库类型的工作负载,使用比 OLTP 类型的工作负载更多的分区更有意义。通常,在数据仓库中,查询计划时间不太重要,因为大部分处理时间都花在查询执行期间。对于这两种类型的工作负载中的任何一种,尽早做出正确的决定很重要,因为重新分区大量数据可能会非常缓慢。预期工作负载的模拟通常有利于优化分区策略。永远不要假设更多的分区比更少的分区更好,反之亦然。

posted @ 2022-09-03 20:57  shigp1  阅读(4756)  评论(0编辑  收藏  举报