2024年9月26日: PostgreSQL 17 发布!
支持的版本:当前 (17) / 16 / 15 / 14 / 13 / 12
开发版本:devel
不受支持的版本:11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2 / 8.1

5.12. 表分区 #

PostgreSQL 支持基本表分区。本节介绍了为什么以及如何在数据库设计中实现分区。

5.12.1. 概述 #

分区是指将逻辑上一个大的表拆分成较小的物理片段。分区可以提供几个好处

  • 在某些情况下,查询性能可以显着提高,尤其是在表的大部分频繁访问的行位于单个分区或少量分区中时。分区有效地替代了索引的上层树级别,从而更有可能使索引的频繁使用部分适合内存。

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

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

  • 很少使用的数据可以迁移到更便宜和更慢的存储介质。

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

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

范围分区 #

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

列表分区 #

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

哈希分区 #

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

如果您的应用程序需要使用上面未列出的其他分区形式,则可以使用替代方法,例如继承和 UNION ALL 视图。这些方法提供了灵活性,但没有内置声明式分区的一些性能优势。

5.12.2. 声明式分区 #

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

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

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

无法将常规表转换为分区表,反之亦然。但是,可以将现有的常规表或分区表作为分区表的子分区添加,或从分区表中删除分区将其转换为独立表;这可以简化和加快许多维护过程。有关 ATTACH PARTITIONDETACH PARTITION 子命令的更多信息,请参阅 ALTER TABLE

分区也可以是 外部表,尽管需要格外小心,因为用户有责任确保外部表的内容满足分区规则。还有一些其他限制。有关更多信息,请参阅 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. 通过指定 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_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 中并映射到 measurement_y2006m02 的任何数据(或直接插入到 measurement_y2006m02 中的数据,只要满足其分区约束即可)将被进一步重定向到其分区之一,具体取决于 peaktemp 列。指定的分区键可能与父分区的分区键重叠,但在指定子分区的边界时应注意,它接受的数据集构成其分区自身边界允许的内容的子集;系统不会尝试检查是否确实如此。

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

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

  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或类似工具备份数据的有用时机。它也可能是在将数据聚合到更小的格式、执行其他数据操作或运行报告的有用时机。命令的第一种形式需要父表上的ACCESS EXCLUSIVE锁。如第二种形式所示,添加CONCURRENTLY限定符允许分离操作仅需要父表上的SHARE UPDATE EXCLUSIVE锁,但请参阅ALTER TABLE ... DETACH PARTITION了解有关限制的详细信息。

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

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约束。不允许在分区表上创建标记为NO INHERITCHECK约束。如果父表中存在相同的约束,则无法删除分区列上的NOT NULL约束。

  • 使用ONLY仅在分区表上添加或删除约束是受支持的,只要不存在分区即可。一旦分区存在,对于UNIQUEPRIMARY KEY之外的任何约束,使用ONLY将导致错误。相反,可以添加分区本身上的约束,并且(如果它们不存在于父表中)可以删除它们。

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

5.12.3. 使用继承进行分区 #

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

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

  • 表继承允许多重继承。

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

5.12.3.1. 示例 #

此示例构建一个等效于上述声明式分区示例的分区结构。请执行以下步骤。

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

    CREATE TABLE measurement (
        city_id         int not null,
        logdate         date not null,
        peaktemp        int,
        unitsales       int
    );
    
  2. 创建几个表,每个表都从根表继承。通常,这些表不会向从根继承的集合中添加任何列。与声明式分区一样,这些表在各个方面都是正常的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. 确保在 postgresql.conf 中未禁用 constraint_exclusion 配置参数;否则可能会不必要地访问子表。

正如我们所看到的,复杂的表层次结构可能需要大量的 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 输出中的 已删除的子计划 属性来确定在此阶段删除的分区数。

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

可以使用 enable_partition_pruning 设置禁用分区剪枝。

5.12.5. 分区和约束排除 #

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

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

约束排除使用 CHECK 约束这一事实,使其速度比分区剪枝慢,有时可以将其用作优势:因为即使在声明式分区表上也可以定义约束,除了其内部分区边界之外,约束排除可能能够从查询计划中省略其他分区。

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

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

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

  • 约束排除仅在查询的 WHERE 子句包含常量(或外部提供的参数)时才有效。例如,针对不可变函数(如 CURRENT_TIMESTAMP)的比较无法优化,因为计划程序无法知道该函数的值在运行时可能属于哪个子表。

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

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

5.12.6. 声明式分区的最佳实践 #

表分区方式的选择应谨慎考虑,因为设计不当会对查询计划和执行的性能产生负面影响。

最关键的设计决策之一是选择用于对数据进行分区的列。通常,最佳选择是按在执行的分区表查询的WHERE子句中最常出现的列或列集进行分区。WHERE子句如果与分区边界约束兼容,则可以用来修剪不需要的分区。但是,PRIMARY KEYUNIQUE约束的要求可能会迫使您做出其他决策。在规划分区策略时,还需要考虑删除不需要的数据。整个分区可以相当快地分离,因此,将分区策略设计成所有要一次删除的数据都位于单个分区中可能是有益的。

选择表应划分成的目标分区数量也是一个关键决策。分区数量不足可能意味着索引仍然太大,并且数据局部性仍然较差,这可能导致缓存命中率低。但是,将表划分为过多分区也会导致问题。过多的分区可能意味着更长的查询计划时间以及在查询计划和执行期间更高的内存消耗,如下文进一步描述。在选择如何对表进行分区时,还必须考虑将来可能发生的更改。例如,如果您选择每个客户一个分区,并且您目前只有少量大型客户,请考虑如果几年后您发现自己拥有大量小型客户,会产生什么影响。在这种情况下,最好选择按HASH进行分区并选择合理的分区数量,而不是尝试按LIST进行分区,并希望客户数量不会超过按此方式对数据进行分区的实用范围。

子分区可用于进一步划分预计将变得比其他分区更大的分区。另一种选择是使用分区键中的多列进行范围分区。这两种方法都可能轻松导致分区数量过多,因此建议谨慎使用。

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

对于数据仓库类型的工作负载,使用比OLTP类型工作负载更多的分区是有意义的。通常,在数据仓库中,查询计划时间不太令人担忧,因为大部分处理时间都花在查询执行上。对于这两种类型的工作负载,务必尽早做出正确的决策,因为重新分区大量数据可能非常缓慢。模拟预期工作负载通常有利于优化分区策略。切勿仅仅假设分区越多越好,反之亦然。

提交更正

如果您在文档中看到任何不正确的内容,与您对特定功能的体验不符,或者需要进一步说明,请使用此表单报告文档问题。