Redrock Postgres 搜索 英文
版本: 9.3 / 9.4 / 9.5 / 9.6 / 10 / 11 / 12 / 13 / 14 / 15 / 16 / 17

5.12. 表分区 #

5.12.1. 概述
5.12.2. 声明式分区
5.12.3. 使用继承进行分区
5.12.4. 分区剪裁
5.12.5. 分区和约束排除
5.12.6. 声明式分区的最佳实践

PostgreSQL 支持基本表分区。本节描述为什么以及如何将分区作为数据库设计的一部分来实现。

5.12.1. 概述 #

分区是指将逻辑上一个大表拆分为更小的物理部分。分区可以提供多种好处

  • 在某些情况下可以大幅提升查询性能,特别是当大部分频繁访问的表行都位于一个分区或少数几个分区中时。分区有效地替代了索引的上层树级别,从而增加了索引中经常使用部分装入内存的可能性。

  • 当查询或更新访问一个分区的很大比例时,可以通过对该分区使用顺序扫描来提升性能,而不是使用索引,使用索引需要对整个表进行分散的随机访问读取。

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

  • 可以将不常用的数据迁移到更便宜、更慢的存储介质中。

这些好处通常只有在表非常大时才值得。表从分区中受益的确切点取决于应用程序,尽管有一条经验法则,即表的大小应超过数据库服务器的物理内存。

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

范围分区 #

该表被划分为由键列或列集定义的 范围,不同分区分配的值范围之间没有重叠。例如,可以按日期范围分区,或按特定业务对象的标识符范围分区。每个范围的边界都被理解为下限为包含,上限为不包含。例如,如果一个分区的范围是从 110,下一个分区的范围是从 1020,则值 10 属于第二个分区而不是第一个分区。

列表分区 #

该表通过显式列出每个分区中出现的键值来分区。

哈希分区 #

该表通过为每个分区指定模量和余数来进行分区。对于分区键的哈希值除以指定模量后会得出指定余数的行来说,每个分区都将容纳这些行。

如果应用程序需要使用上述未列出的其他形式的分区,则可以使用替代方法,如继承和 UNION ALL 视图。这种方法非常灵活,但没有基于声明划分的内置性能优势。

5.12.2. 声明分区 #

PostgreSQL 允许声明表可以划分为多个分区。划分为多个分区的表称为分区表。该声明包含上面描述的分区方法,以及将作为分区键使用的列或表达式的列表。

分区表本身是一个虚拟表,没有自己的存储空间。相反,存储空间属于分区,即与该分区表关联的其他普通表。每个分区都存储由其分区边界定义的数据子集。插入到分区表中的所有行都将根据分区键列的值路由到相应的分区。更新该行的分区键的话,如果不再满足其原始分区的边界条件,它会移动到其他分区。

分区还可以定义为分区表,从而实现子分区。尽管所有分区都必须与其分区父分区拥有相同的列,但分区可以拥有自己的索引、约束和默认值,不同于其他分区。有关创建分区表和分区的更多详细信息,请参见CREATE TABLE

无法将常规表变成分区表,反之亦然。不过,可以将现有常规表或分区表添加为分区表的分区,或者从分区表中移除分区,将其变为独立表。这可以简化和加快许多维护进程。请参阅ALTER TABLE,了解有关子命令ATTACH PARTITIONDETACH PARTITION的详细信息。

分区也可以是外部表,但需要非常小心,因为届时用户需要确保外部表的内容满足分区规则。还有一些其他限制。有关详细信息,请参阅CREATE FOREIGN TABLE

5.12.2.1. 示例 #

假设我们要为一家大型冰淇淋公司构建一个数据库。该公司会每天测量最高温度,以及各个地区冰淇淋的销量。从概念上讲,我们需要一张如下所示的表

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

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

在这种情况下,要使用声明式分区,请执行以下步骤

  1. 通过指定包含分区方法(在这种情况下为RANGE)和要用作分区键的列列表的PARTITION BY子句,将measurement表创建为分区表。

    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_y2006m02 PARTITION OF measurement
        FOR VALUES FROM ('2006-02-01') TO ('2006-03-01');
    
    CREATE TABLE measurement_y2006m03 PARTITION OF measurement
        FOR VALUES FROM ('2006-03-01') TO ('2006-04-01');
    
    ...
    CREATE TABLE measurement_y2007m11 PARTITION OF measurement
        FOR VALUES FROM ('2007-11-01') TO ('2007-12-01');
    
    CREATE TABLE measurement_y2007m12 PARTITION OF measurement
        FOR VALUES FROM ('2007-12-01') TO ('2008-01-01')
        TABLESPACE fasttablespace;
    
    CREATE TABLE measurement_y2008m01 PARTITION OF measurement
        FOR VALUES FROM ('2008-01-01') TO ('2008-02-01')
        WITH (parallel_workers = 4)
        TABLESPACE fasttablespace;
    

    (请记住,相邻分区可以共享界限值,因为范围上限被视为独占上限。)

    如果您希望实施子分区,那么在用于创建各个分区的命令中再次指定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_y2006m02measurement中的数据(或直接插入到measurement_y2006m02中的数据,只要满足其分区约束,允许这样做),都将进一步根据peaktemp列重定向到其某个分区。指定的 partition key 可能与父 partition key 重叠,尽管在指定分区的分界时应格外小心,以使它接受的数据集成为分区自身分界允许的子集;系统不会尝试检查是否实际上是这种情况。

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

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

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

    CREATE INDEX ON measurement (logdate);
    
  4. 确保enable_partition_pruning配置参数在postgresql.conf中未禁用。如果禁用,则不会按所需优化查询。

在上述示例中,我们每个月都将创建一个新分区,因此最好编写一个脚本自动生成所需的DDL。

5.12.2.2. 分区维护#

通常,在最初定义表时建立的一组分区并不打算保持静态。通常需要删除保存舊数据的分区,并定期添加新分区以获取新数据。分区的一个最重要的优点恰恰在于它通过操控分区结构(而不是在大量数据周围进行物理移动)来实现这个本来很痛苦的任务几乎可以立即执行。

删除旧数据的最简单选项是取消不再需要的分区

DROP TABLE measurement_y2006m02;

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

另一个经常更可取的选项是从分区表中删除分区,但保留对它作为其自身的表进行访问。这有两种形式

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

这些操作允许在删除数据之前,对数据进行进一步操作。例如,这通常是在使用 COPYpg_dump 或类似工具备份数据时使用。

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

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

作为创建新分区的替代方法,有时创建与分区结构分开的表并稍后将它附加为分区会更方便。这允许在新数据出现在分区表之前对其进行加载、检查和转换。此外,ATTACH PARTITION 操作只需要在分区表上获取 SHARE UPDATE EXCLUSIVE 锁,而不是 CREATE TABLE ... PARTITION OF 所需的 ACCESS EXCLUSIVE 锁,因此它对分区表上的并行操作更友好;请参阅 ALTER TABLE ... ATTACH PARTITION 以获取其他详细信息。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' );

\copy measurement_y2008m02 from 'measurement_y2008m02'
-- possibly some other data preparation work

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

请注意,在运行 ATTACH PARTITION 命令时,将在该分区上持有 ACCESS EXCLUSIVE 锁的同时扫描表以验证分区约束。如上所示,建议通过在附加表之前,在表上创建与预期分区约束匹配的 CHECK 约束来避免这种扫描。完成 ATTACH PARTITION 后,建议删除现在多余的 CHECK 约束。如果被附加的表本身是一个分区表,那么它的每个子分区将被递归锁定并扫描,直到遇到合适的 CHECK 约束或到达叶分区为止。

类似地,如果分区表具有 DEFAULT 分区,建议创建一个排除要附加的分区的约束的 CHECK 约束。如果不这样做,DEFAULT 分区将被扫描以验证它不包含应该在要附加的分区中找到的记录。此操作将在 DEFAULT 分区上保持 ACCESS EXCLUSIVE 锁时执行。如果 DEFAULT 分区本身是一个分区表,则它的每个分区将以与以上提到的要附加的表相同的方式进行递归检查。

如前所述,可以在分区表上创建索引,以便它们自动应用于整个层次结构。这非常方便,不仅所有现有分区都将被索引,而且所有未来分区也将被索引。但是,在分区表上创建新索引时的一个限制是不能使用 CONCURRENTLY 限定符,这可能会导致较长的锁定时间。为避免这种情况,您可以在分区表上仅使用 CREATE INDEX ON ONLY,它创建的新索引标记为无效,防止自动应用于现有分区。相反,可以在每个分区上使用 CONCURRENTLY 单独创建索引,然后使用 ALTER INDEX ... ATTACH PARTITION附加 到父级的分区索引。一旦所有分区索引被附加到父级索引,父级索引将自动标记为有效。示例

CREATE INDEX measurement_usls_idx ON ONLY measurement (unitsales);

CREATE INDEX CONCURRENTLY measurement_usls_200602_idx
    ON measurement_y2006m02 (unitsales);
ALTER INDEX measurement_usls_idx
    ATTACH PARTITION measurement_usls_200602_idx;
...

此技术也可以与 UNIQUEPRIMARY 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;
...

5.12.2.3. 限制 #

以下限制适用于分区表

  • 要在分区表上创建唯一或主键约束,分区键不得包含任何表达式或函数调用,并且约束的列必须包含所有分区键列。此限制存在是因为组成该约束的各个索引只能在其自己的分区内直接强制唯一性;因此,分区结构本身必须保证不同分区中不存在重复项。

  • 同样,排斥约束必须包括所有分区键列。此外,约束必须比较这些列以求得相等(而不是例如 &&)。同样,此限制源于无法强制跨分区限制。约束可能包括不属于分区键的部分其他列,并且可以将它们与任何运算符进行比较。

  • BEFORE ROW 触发器在 INSERT 上触发,不能更改哪个分区是新行的最终目标。

  • 不允许在同一个分区树中混用临时和永久关系。因此,如果已分区表是永久的,则其分区也必须是永久的,同样,如果已分区表是临时的,则其分区也必须是临时的。使用临时关系时,分区树中的所有成员都必须来自同一个会话。

各个分区在其背后的继承关系中链接到它们的分区表。然而,不可能对具有声明性分区的表或其分区使用继承的所有通用特性,如下所述。值得注意的是,分区不能有任何父分区(除它本身所属的分区表之外),表也不能同时从分区表和常规表中继承。这意味着分区表及其分区绝不会与常规表共享一个继承层次结构。

由于由分区表及其分区组成的分区层次结构仍然是一个继承层次结构,tableoid 和所有正常的继承规则均适用,如 第 5.11 节 中所述,但有一些例外

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

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

  • 使用 ONLY 来仅在分区表上添加或删除约束受到支持,前提是没有分区。一旦分区存在,对 UNIQUEPRIMARY KEY 以外の任何约束使用 ONLY 都会导致一个错误。相反,可以添加分区本身的约束,并且(如果它们不存在父表中)可以删除它们。

  • 由于分区表本身没有任何数据,尝试在分区表上使用 TRUNCATE ONLY 将总是返回一个错误。

5.12.3. 使用继承进行分区 #

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

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

  • 表继承允许多重继承。

  • 声明分区只支持范围、列表和哈希分区,而表继承允许以用户选择的方式对数据进行划分。(但请注意,如果约束排除不能有效地修剪子表,则查询性能可能会很差。)

5.12.3.1. 示例 #

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

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

    CREATE TABLE measurement (
        city_id         int not null,
        logdate         date not null,
        peaktemp        int,
        unitsales       int
    );
    
  2. 创建几个 child 表,每个表都从根表继承。通常,这些表不会向从根继承的集合中添加任何列。就像声明分区一样,这些表在各个方面都是正常的 PostgreSQL 表(或外部表)。

    CREATE TABLE measurement_y2006m02 () INHERITS (measurement);
    CREATE TABLE measurement_y2006m03 () INHERITS (measurement);
    ...
    CREATE TABLE measurement_y2007m11 () INHERITS (measurement);
    CREATE TABLE measurement_y2007m12 () INHERITS (measurement);
    CREATE TABLE measurement_y2008m01 () 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 )
    

    这是错误的,因为不清楚 200 的关键值属于哪个子表。相反,应该用这种样式定义范围

    CREATE TABLE measurement_y2006m02 (
        CHECK ( logdate >= DATE '2006-02-01' AND logdate < DATE '2006-03-01' )
    ) INHERITS (measurement);
    
    CREATE TABLE measurement_y2006m03 (
        CHECK ( logdate >= DATE '2006-03-01' AND logdate < DATE '2006-04-01' )
    ) INHERITS (measurement);
    
    ...
    CREATE TABLE measurement_y2007m11 (
        CHECK ( logdate >= DATE '2007-11-01' AND logdate < DATE '2007-12-01' )
    ) INHERITS (measurement);
    
    CREATE TABLE measurement_y2007m12 (
        CHECK ( logdate >= DATE '2007-12-01' AND logdate < DATE '2008-01-01' )
    ) INHERITS (measurement);
    
    CREATE TABLE measurement_y2008m01 (
        CHECK ( logdate >= DATE '2008-01-01' AND logdate < DATE '2008-02-01' )
    ) INHERITS (measurement);
    
  4. 对于每个子表,对关键列以及您想要的任何其他索引创建索引。

    CREATE INDEX measurement_y2006m02_logdate ON measurement_y2006m02 (logdate);
    CREATE INDEX measurement_y2006m03_logdate ON measurement_y2006m03 (logdate);
    CREATE INDEX measurement_y2007m11_logdate ON measurement_y2007m11 (logdate);
    CREATE INDEX measurement_y2007m12_logdate ON measurement_y2007m12 (logdate);
    CREATE INDEX measurement_y2008m01_logdate ON measurement_y2008m01 (logdate);
    
  5. 我们希望应用程序能够说 INSERT INTO measurement ...,并且数据能够重定向到适当的子表中。我们可以通过将合适的触发器函数附加到根表来安排这样做。如果数据将仅被添加到最新的子中,我们可以使用一个非常简单的触发器函数

    CREATE OR REPLACE FUNCTION measurement_insert_trigger()
    RETURNS TRIGGER AS $$
    BEGIN
        INSERT INTO measurement_y2008m01 VALUES (NEW.*);
        RETURN NULL;
    END;
    $$
    LANGUAGE plpgsql;
    

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

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

    我们必须每个月重新定义该触发器函数,这样它始终会插入到当前的子表中。但是,不需要更新该触发器定义。

    我们可能希望插入数据,并且使服务器自动找到将行添加到其中的子表。我们可以通过更复杂的触发器函数来实现这一点,例如

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

    该触发器定义与以前相同。请注意,每个 IF 测试都必须完全匹配其子表的 CHECK 约束。

    虽然该函数比单月情况更复杂,但不需要经常更新,因为可以在需要之前添加分支。

    注意

    在实践中,如果大多数插入进入该子,首先检查最新的子可能是最好的。为了简单起见,我们已在示例的其他部分中显示了触发器的测试。

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

    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.*);
    ...
    CREATE RULE measurement_insert_y2008m01 AS
    ON INSERT TO measurement WHERE
        ( logdate >= DATE '2008-01-01' AND logdate < DATE '2008-02-01' )
    DO INSTEAD
        INSERT INTO measurement_y2008m01 VALUES (NEW.*);
    

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

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

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

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

正如我们所看到的,复杂的表层次结构可能需要大量的 DDL。在上述示例中,我们将在每个月创建一个新的子表,因此编写一个自动生成所需 DDL 的脚本可能是明智的。

5.12.3.2. 继承分区维护 #

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

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;

5.12.3.3. 注意事项 #

以下注意事项适用于使用继承实现的分区

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

  • 索引和外键约束适用于单个表,而不适用于它们的继承子表,因此它们有一些需要注意的 注意事项

  • 此处显示的模式假定一行键列的值永远不会更改,或者至少不会更改到需要将其移动到另一个分区。尝试这样做的 UPDATE 将由于 CHECK 约束而失败。如果您需要处理此类情况,则可以在子表上放置合适的更新触发器,但这会使结构管理复杂得多。

  • 如果您使用手动 VACUUMANALYZE 命令,请别忘记您需要在每个子表上单独运行这些命令。类似于下面这样的命令

    ANALYZE measurement;
    

    只会处理根表。

  • INSERT 语句与 ON CONFLICT 子句不太可能按预期工作,因为 ON CONFLICT 操作只在指定目标关系上的唯一约束违规情况下才会触发,而不在其子关系上触发。

  • 除非应用程序明确了解分区方案,否则将需要触发器或规则将行路由到所需的子表。触发器可能很难编写,而且将比声明式分区内部执行的元组路由慢得多。

5.12.4. 分区修剪 #

分区修剪是一种查询优化技术,它可以提升声明式分区表的性能。例如

SET enable_partition_pruning = on;                 -- the default
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 '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)

请注意,分区剪枝仅受分区键隐式定义的约束驱动,不受索引是否存在的影响。因此,无需在键列上定义索引。是否需要为给定的分区创建索引取决于你是否预期扫描分区的查询通常会扫描分区的大部分还是只扫描一小部分。在后一种情况下,索引将很有帮助,但在前一种情况下则不然。

不仅可以在给定查询的计划中执行分区剪枝,还可以在其执行期间执行分区剪枝。这非常有用,因为它可以在语句中包含其值在查询计划时未知的表达式的子句的情况下剪枝更多分区,例如在 PREPARE 语句中定义的参数,使用从子查询获取的值,或在嵌套循环联接的内侧使用参数化值。在执行过程中进行分区剪枝可以在以下任何时间执行

  • 在查询计划初始化期间。分区剪枝可以在这里对在执行初始化阶段已知的参数值执行。在此阶段剪枝的分区将不会显示在查询的 EXPLAINEXPLAIN ANALYZE 中。可以通过观察 EXPLAIN 中的 Subplans Removed 属性来确定在此阶段已删除的分区数。输出。

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

可以通过使用 enable_partition_pruning 设置来禁用分区剪裁。

5.12.5 分区和约束排除 #

约束排除也是一种查询优化技术,类似于分区剪裁。尽管它最初用于通过旧式继承方法来执行的分区,它也可以用于其他目的,比如声明性分区。

约束排除采取的方法与分区剪裁非常类似,唯一的差异在于它利用每张表的 CHECK 约束,而非分区剪裁利用的表的 partition 范围(仅在声明性分区中存在)。另一个差异是约束排除只应用在规划时间,不尝试在执行时间删除分区。

与分区剪裁相比,约束排除利用 CHECK 约束的事实使它运行速度较慢,但这有时也被用作一种优势:因为除了内部的分区范围,约束甚至可以被定义在声明性分区中,所以约束排除能够从查询计划中过滤更多分区。

constraint_exclusion 的默认(也是建议的)设置既不是 on 也不是 off,而是一种叫做 partition 的中间设置,该设置导致了该技术只应用于很可能处理继承分区表的查询。on 设置导致计划程序检查所有查询中的 CHECK 约束,甚至不太可能受益的简单查询。

约束排除适用以下警告

  • 与可以在查询执行期间应用的分区剪裁不同,约束排除只在查询规划期间应用。

  • 只有当查询的 WHERE 子句包含常量(或外部提供的参数)时,约束排除才会起作用。例如,一个针对 CURRENT_TIMESTAMP 等非不可变函数的比较是无法优化的,因为计划程序无法知道该函数的值在运行时可能会落在哪个子表中。

  • 使分区约束保持简单,否则计划程序无法证明子表无需访问。如在前面的例子中所展示的那样,为列表分区使用简单的相等条件,或为范围分区使用简单的范围测试。一个好的经验法则就是分区约束只应包含对使用 B 树可索引运算符的分区列与常量的比较,因为只有 B 树可索引列才能被允许作为分区键。

  • 在约束排除期间将检查父表的所有子表的全部约束,因此大量子表可能会显著延长查询计划时间。因此,基于传统继承的分区将适用于多达大约一百个子表;不要尝试使用数千个子表。

5.12.6. 自声明分区最佳实践 #

应该仔细选择分区表的方式,因为查询计划和执行的性能可能会受到糟糕的设计的负面影响。

最关键的设计决策之一将是分区数据的列或列。通常最佳选择将是分区按要执行查询的已分区表 WHERE 子句中通常显示的列或列集合来进行分区。 WHERE 与分区边界约束兼容的子句可用于去掉不需要的分区。然而,可以根据 PRIMARY KEYUNIQUE 约束强制做出其他决策。在计划分区策略时,删除不需要的数据也是一个需要考虑的因素。一个完整的分区可以相当快速地脱离,因此设计分区策略的方式可能会带来好处,即一次要删除的所有数据都位于单个分区中。

选择要将表划分的目标分区数也是要做的关键决策。没有足够的分区可能表示索引仍然太大,并且数据局部性仍然较差,这可能导致低缓存命中率。然而,将表划分为太多分区也会导致问题。太多分区可能表示查询计划时间较长,并且查询计划和执行期间内存消耗较高,如下所述。在选择表的分区方式时,考虑将来可能会发生哪些变化也很重要。例如,如果您选择对每个客户分一个分区,并且当前有少量大客户,请考虑将来如果您的客户变成大量小客户会产生的影响。在这种情况下,最好选择按 HASH 进行分区,并且选择合理数量的分区,而不是尝试按 LIST 进行分区,并希望客户的数量不会增加到无法按其对数据进行分区的地步。

子分区对于进一步划分预期会变得比其他分区更大的分区很有用。另一个选择是在分区键中使用多个列进行范围分区。其中任何一个都可能轻松导致分区数量过多,因此,谨慎是很明智的。

在查询计划和执行过程中,考虑分区开销非常重要。查询规划器通常能够很好地处理拥有最多几千个分区的层级分区,前提是典型查询允许查询规划器修剪除少量分区以外的所有分区。在规划器执行分区修剪后,如果剩余更多分区,则规划时间会变长,内存消耗也会变高。需要关注另一个拥有大量分区的问题是,服务器的内存消耗随着时间推移会大幅增长,尤其是如果许多会话涉及大量分区时。这是因为,每个分区都要求将其元数据加载到访问它的每个会话的本地内存中。

对于数据仓库类型的工作负载,与OLTP类型的工作负载相比,使用更多分区可能更有意义。通常,在数据仓库中,查询规划时间不是一个大问题,因为大部分处理时间都花在查询执行期间。对于这两种工作负载类型中的任何一种,尽早做出正确的决策非常重要,因为重新对大量数据分区会非常慢。针对预期工作负载进行的模拟通常有助于优化分区策略。切不可想当然地认为更多分区优于更少分区,反之亦然。