2025年9月25日: PostgreSQL 18 发布!
支持的版本: 当前 (18) / 17 / 16 / 15 / 14 / 13
开发版本: devel
不支持的版本: 12 / 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

不能将普通表转换为分区表,反之亦然。但是,可以将现有的普通表或分区表添加为分区表的一个分区,或从分区表中删除一个分区,使其成为一个独立的表;这可以简化和加速许多维护过程。请参阅 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. 通过指定 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. 确保 postgresql.conf 中的 enable_partition_pruning 配置参数未被禁用。如果禁用,查询将不会按预期进行优化。

在上面的示例中,我们每个月都会创建一个新分区,因此编写一个自动生成所需 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 PARTITIONCREATE 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 约束来避免此扫描。附加完成后,建议删除现在冗余的 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 ROWINSERT 触发器,不能更改新行的最终目标分区。

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

分区通过底层继承与它们的分区表链接。但是,不能将继承的所有通用功能用于声明式分区表或其分区,如下文所述。特别是,分区不能有除其所在分区表之外的任何父表,也不能从分区表和普通表继承。这意味着分区表及其分区从不与普通表共享继承层次结构。

由于由分区表及其分区组成的分区层次结构仍然是继承层次结构,因此 tableoid 和继承的所有正常规则都适用,如 第 5.11 节中所述,但有几处例外:

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

  • 分区表的 CHECKNOT NULL 约束始终由其所有分区继承;不允许创建这些类型的 NO INHERIT 约束。如果父表中存在相同的约束,则不能删除这些类型的约束。

  • 使用 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 命令会自动处理所有继承子表。如果这是不希望的,可以使用 ONLY 关键字。例如,命令:

    ANALYZE ONLY measurement;
    

    将只处理根表。

  • 带有 ON CONFLICT 子句的 INSERT 语句不太可能按预期工作,因为 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 输出中显示的分区,而不是“已移除的子计划”属性引用的那些分区。

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

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

5.12.5. 分区与约束排除 #

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

约束排除的工作方式与分区剪枝非常相似,不同之处在于它使用每个表的 CHECK 约束——这就是它得名原因——而分区剪枝使用表的“分区边界”,后者仅在声明式分区的情况下存在。另一个不同之处在于,约束排除仅在规划时应用;在执行时不会尝试移除分区。

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

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

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

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

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

  • 保持分区约束简单,否则规划器可能无法证明子表可能不需要被访问。对于列表分区,使用简单的相等性条件;对于范围分区,使用简单的范围测试,如前面示例所示。一个好的经验法则是,分区约束应仅包含使用 B-tree 可索引运算符将分区列与常量进行比较,因为只有 B-tree 可索引的列允许在分区键中使用。

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

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

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

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

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

子分区可以用于进一步划分预计会比其他分区大的分区。另一种选择是使用具有多个分区键列的范围分区。这两种方法都可能轻易导致分区过多,因此建议谨慎。

重要的是要考虑分区在查询规划和执行期间的开销。查询规划器通常可以很好地处理具有数千个分区的分区层次结构,前提是典型查询允许查询规划器将分区剪枝到只剩少量分区。当规划器执行分区剪枝后剩余的分区数量较多时,规划时间会变长,内存消耗会变高。拥有大量分区的另一个原因是服务器的内存消耗可能会随着时间的推移而显着增长,特别是如果许多会话访问大量分区。这是因为每个分区都需要将其元数据加载到访问它的每个会话的本地内存中。

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

提交更正

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