↑↓ 选择↵ 打开⌫ 切换范围完整搜索

PG.CENTER 连接 PostgreSQL 文档、百科与生态知识。由 Pigsty 维护。

支持中的版本: 当前版本 (18) / 17 / 16 / 15 / 14
开发中的版本: 19 / 20devel
已结束支持的版本: 13 / 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
历史版本。 PostgreSQL 10 已结束支持。 2022-11-10. 请参阅 当前版本手册.

5.10. 表分区 #

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

5.10.1. 概述 #

分区是指将逻辑上的一个大表拆分成较小的物理部分。分区可以带来以下好处:

  • 在某些情况下查询性能能够显著提升,特别是当表中频繁访问的行大多集中在一个或少数几个分区时。分区替代了索引的前导列,从而减小索引大小,使索引中被大量使用的部分更有可能容纳在内存中。

  • 当查询或更新需要访问单个分区中的很大一部分数据时,顺序扫描该分区可以提高性能,优于使用索引并在整个表中执行分散的随机读取。

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

  • 很少使用的数据可以被迁移到便宜且较慢的存储介质上。

通常只有表本身非常大时,这些好处才值得考虑。表从多大开始受益于分区,取决于具体应用;不过,一条经验法则是表的大小应超过数据库服务器的物理内存。

PostgreSQL 内置支持以下几种分区形式:

范围分区

按照某个键列或一组列定义的“范围”对表进行分区,不同分区所分配的值范围互不重叠。例如,可以按日期范围,或特定业务对象的标识符范围分区。

列表分区

通过明确列出每个分区包含哪些键值来对表进行分区。

如果应用程序需要使用上面未列出的分区形式,可以采用继承和 UNION ALL 视图等替代方法。这些方法具有灵活性,但不具备内置声明式分区的某些性能优势。

5.10.2. 声明式分区 #

PostgreSQL 提供了一种方式,用于指定如何将表划分为称为分区的各个部分。被划分的表称为分区表。该定义包括分区方法,以及用作分区键的列或表达式列表。

插入到分区表中的所有行都会根据分区键的值路由到某个分区。每个分区包含由其分区边界定义的数据子集。目前支持的分区方法包括范围分区和列表分区,分别为每个分区分配一段键范围和一组键列表。

分区本身也可以定义为分区表,这称为子分区。分区可以具有自己的索引、约束和默认值,与其他分区不同。必须为每个分区单独创建索引。关于创建分区表和分区的更多细节,参见 CREATE TABLE。

不能将普通表转换为分区表,反之亦然。不过,可以将包含数据的普通表或分区表作为分区添加到分区表,也可以从分区表中移除分区,使其成为独立的表;有关 ATTACH PARTITION 和 DETACH PARTITION 子命令的更多信息,参见 ALTER TABLE。

各个分区在内部通过继承与分区表关联;不过,上一节讨论的一些继承功能不能用于分区表及其分区。例如,分区不能具有其所属分区表以外的父表,普通表也不能以分区表为父表进行继承。这意味着分区表和分区不参与与普通表之间的继承。由于分区表及其分区所组成的分区层次仍然是继承层次,tableoid 以及所有普通继承规则仍然适用,具体见第 5.9 节,但有一些例外,最主要的是:

  • 分区表的 CHECK 和 NOT NULL 约束总会被其所有分区继承。不允许在分区表上创建标记为 NO INHERIT 的 CHECK 约束。

  • 没有分区时,可以使用 ONLY 仅在分区表上添加或删除约束。一旦存在分区,使用 ONLY 就会报错,因为存在分区时,不支持仅在分区表上添加或删除约束。可以直接在分区上添加或删除父表中不存在的约束。由于分区表不直接存放数据,尝试对分区表使用 TRUNCATE ONLY 总会报错。

  • 分区不能具有父级中不存在的列。在使用 CREATE TABLE 创建分区时无法指定列,也无法使用 ALTER TABLE 在事后添加列到分区。只有当表的列与父级完全匹配(包括任何 oid 列)时,才能使用 ALTER TABLE ... ATTACH PARTITION 将表添加为分区。

  • 如果父表中存在 NOT NULL 约束,就不能删除分区列上的该约束。

分区也可以是外部表(见 CREATE FOREIGN TABLE),但它们具有普通表没有的一些限制。例如,插入到分区表的数据不会被路由到外部表分区。

5.10.2.1. 示例 #

假设正在为一家大型冰淇淋公司构建数据库。该公司每天测量最高温度,并统计各个地区的冰淇淋销量。从概念上看,我们需要如下表:

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

我们知道,大多数查询只会访问最近一周、一个月或一个季度的数据,因为该表主要用于为管理层生成在线报表。为减少需要存储的旧数据量,决定只保留最近 3 年的数据,并在每月月初删除最早一个月的数据。在这种情况下,可以利用分区来满足测量表的各种需求。

在这种情况下使用声明式分区,可按以下步骤操作:

  1. 创建 measurement 表时,将它定义为分区表,指定 PARTITION BY 子句,其中包括分区方法(本例为 RANGE)及用作分区键的列列表。

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

    如果需要,可以在范围分区的分区键中使用多列。当然,这通常会产生更多分区,而各个分区更小。反过来,使用较少的列可能会使分区条件更粗,从而产生较少的分区。如果查询条件涉及这些列中的部分或全部列,访问分区表的查询就只需扫描较少的分区。例如,考虑一个依次使用 lastname 和 firstname 作为分区键的范围分区表。

  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_y2006m02 (logdate);
    CREATE INDEX ON measurement_y2006m03 (logdate);
    ...
    CREATE INDEX ON measurement_y2007m11 (logdate);
    CREATE INDEX ON measurement_y2007m12 (logdate);
    CREATE INDEX ON measurement_y2008m01 (logdate);

  4. 确保 constraint_exclusion 配置参数在 postgresql.conf 中没有被禁用。如果被禁用,查询将不会按照想要的方式被优化。

在上面的示例中,我们会每个月创建一个新分区,因此写一个脚本来自动生成所需的 DDL 会更好。

5.10.2.2. 分区维护 #

通常在初始定义分区表时建立的分区并非保持静态不变。移除存放旧数据的分区并且为新数据周期性地增加新分区的需求比比皆是。分区的最大好处之一就是可以通过操纵分区结构来近乎瞬时地执行这类让人头痛的任务,而不是物理地移动大量数据。

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

DROP TABLE measurement_y2006m02;

这可以非常快速地删除数百万行记录,因为它不需要逐行删除。不过要注意,上面的命令需要在父表上获取 ACCESS EXCLUSIVE 锁。

另一个通常更合适的选择是将分区从分区表中分离,但保留它作为独立表的可访问性:

ALTER TABLE measurement DETACH PARTITION measurement_y2006m02;

这样就可以在删除数据前继续对其执行操作。例如,这时通常适合使用 COPY,pg_dump 或类似工具备份数据,也适合将数据聚合为更紧凑的形式、执行其他数据操作或运行报表。

类似地,可以添加新分区来处理新数据。可以像上面创建初始分区那样,在分区表中创建空分区:

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

另一种有时更方便的做法是先在分区结构之外创建新表,之后再将其变为正式分区。这样,数据在出现在分区表中之前,就可以先被载入、检查和转换:

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 命令之前,建议在要附加的表上创建一个匹配预期分区约束的 CHECK 约束。这样,系统就能够跳过验证隐式分区约束所需的扫描。没有 CHECK 约束,表将在持有父表的 ACCESS EXCLUSIVE 锁的情况下进行扫描以验证分区约束。可以在 ATTACH PARTITION 完成后删除多余的 CHECK 约束。

5.10.2.3. 限制 #

分区表有以下限制:

  • 没有可用机制能在所有分区上自动创建相匹配的索引,必须使用单独命令为每个分区添加索引。这也意味着无法创建跨越所有分区的主键、唯一约束或排他约束;只能分别约束各个叶子分区。

  • 由于分区表不支持主键,因此也不支持引用分区表的外键,或从分区表引用其他表的外键。

  • 对分区表使用 ON CONFLICT 子句会报错,因为唯一约束或排他约束只能在单个分区上创建。不支持跨整个分区层次强制唯一性或排他约束。

  • 导致行从一个分区移动到另一个分区的 UPDATE 会失败,因为行的新值不满足原分区的隐式分区约束。

  • 如果需要行触发器,必须在各个分区上定义,而不能在分区表上定义。

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

5.10.3. 使用继承实现 #

虽然内置的声明式分区适用于大多数常见场景,但有时更灵活的方法可能更有用。可以使用表继承来实现分区,它允许使用声明式分区所不支持的一些功能,例如:

  • 分区强制要求所有分区具有与父表完全相同的列集合,但表继承允许子表具有父表中不存在的额外列。

  • 表继承允许多继承。

  • 声明式分区只支持列表分区和范围分区,而表继承允许按用户选择的方式划分数据。(不过,请注意,如果约束排除不能有效剪枝分区,查询性能会很差。)

  • 某些操作在使用声明式分区时,比使用表继承时需要更强的锁。例如,向分区表添加分区或从中移除分区,都需要在父表上取得 ACCESS EXCLUSIVE 锁,而对于普通继承,SHARE UPDATE EXCLUSIVE 锁就足够了。

5.10.3.1. 示例 #

使用上面未分区的 measurement 表。要使用继承实现分区,请按以下步骤操作:

  1. 创建“根”表,所有分区都将从它继承。这个表将不包含数据。不要在这个表上定义任何检查约束,除非想让它们应用到所有分区上。同样,在这个表上定义索引或者唯一约束也没有意义。对于我们的示例来说,根表是最初定义的 measurement 表。

  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 PROCEDURE 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.10.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.10.3.3. 注意事项 #

使用继承实现的分区表有以下注意事项:

  • 没有自动方式验证所有 CHECK 约束是否互斥。与手工逐个编写相比,通过代码生成分区并创建或修改相关对象更安全。

  • 这里展示的方案假定行的分区键列值永不改变,或者至少不会改变到必须把该行移入另一个分区的程度。由于 CHECK 约束的存在,试图那样做的 UPDATE 将会失败。如果需要处理这种情况,可以在分区表上放置合适的更新触发器,但这会让整个结构的管理复杂得多。

  • 如果手动执行 VACUUM 或 ANALYZE 命令,不要忘记需要在每个分区上分别运行。例如,以下命令:

    ANALYZE measurement;

    只会处理根表。

  • 带有 ON CONFLICT 子句的 INSERT 语句不太可能按照预期工作,因为只有在指定的目标关系而不是其子关系上发生唯一违背时才会采取 ON CONFLICT 动作。

  • 将会需要触发器或者规则将行路由到所需分区中,除非应用明确地知道分区方案。编写触发器可能会很复杂,并且会比声明式分区在内部执行的元组路由慢很多。

5.10.4. 分区和约束排除 #

约束排除是一种查询优化技术,可提高按上述方式定义的分区表的性能(包括声明式分区表和使用继承实现的分区表)。例如:

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

如果没有约束排除,上述查询会扫描 measurement 表的每个分区。启用约束排除后,规划器会检查每个分区的约束,并尝试证明该分区不需要扫描,因为它不可能包含满足查询 WHERE 子句的行。如果规划器能够证明这一点,就会将该分区排除在查询计划之外。

可以使用 EXPLAIN 命令,显示启用 constraint_exclusion 时的计划与禁用时的计划之间的差异。对于这种表结构,典型的未优化计划如下:

SET constraint_exclusion = off;
EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01';

                                          QUERY PLAN
-----------------------------------------------------------------------------------------------
 Aggregate  (cost=158.66..158.68 rows=1 width=0)
   ->  Append  (cost=0.00..151.88 rows=2715 width=0)
         ->  Seq Scan on measurement  (cost=0.00..30.38 rows=543 width=0)
               Filter: (logdate >= '2008-01-01'::date)
         ->  Seq Scan on measurement_y2006m02 measurement  (cost=0.00..30.38 rows=543 width=0)
               Filter: (logdate >= '2008-01-01'::date)
         ->  Seq Scan on measurement_y2006m03 measurement  (cost=0.00..30.38 rows=543 width=0)
               Filter: (logdate >= '2008-01-01'::date)
...
         ->  Seq Scan on measurement_y2007m12 measurement  (cost=0.00..30.38 rows=543 width=0)
               Filter: (logdate >= '2008-01-01'::date)
         ->  Seq Scan on measurement_y2008m01 measurement  (cost=0.00..30.38 rows=543 width=0)
               Filter: (logdate >= '2008-01-01'::date)

某些或全部分区可能使用索引扫描,而不是全表顺序扫描,但这里的重点是,为回答该查询,根本不需要扫描较旧的分区。启用约束排除后,可以得到代价明显更低、结果却相同的计划:

SET constraint_exclusion = on;
EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01';
                                          QUERY PLAN
-----------------------------------------------------------------------------------------------
 Aggregate  (cost=63.47..63.48 rows=1 width=0)
   ->  Append  (cost=0.00..60.75 rows=1086 width=0)
         ->  Seq Scan on measurement  (cost=0.00..30.38 rows=543 width=0)
               Filter: (logdate >= '2008-01-01'::date)
         ->  Seq Scan on measurement_y2008m01 measurement  (cost=0.00..30.38 rows=543 width=0)
               Filter: (logdate >= '2008-01-01'::date)

注意,约束排除只由 CHECK 约束驱动,与是否存在索引无关。因此,不必在键列上定义索引。是否需要为某个分区创建索引,取决于预期查询扫描该分区时通常会扫描其中的大部分,还是仅扫描一小部分。后一种情况下索引会有帮助,前一种情况下则不会。

constraint_exclusion 的默认(也是推荐的)设置不是 on 也不是 off,而是一种被称为 partition 的中间设置,这会导致该技术仅被应用于可能访问分区表的查询。on 设置导致规划器检查所有查询中的 CHECK 约束,甚至是那些不太可能受益的简单查询。

继承表和分区表都会使用约束排除,它有以下注意事项:

  • 只有查询的 WHERE 子句包含常量(或者外部提供的参数)时,约束排除才能有效果。例如,针对一个非不可变函数(如 CURRENT_TIMESTAMP)的比较不能被优化,因为规划器不知道该函数的值在运行时会落到哪个分区中。

  • 保持分区约束尽量简单,否则规划器可能无法证明哪些分区不需要访问。如前面的示例所示,对列表分区使用简单的等值条件,对范围分区使用简单的范围测试。一条很好的经验法则是:分区约束应只包含分区列与常量之间使用 B-树可索引操作符的比较,这一点也适用于分区表,因为只有 B-树可索引列才允许出现在分区键中。(使用声明式分区时,这不成问题,因为自动生成的约束足够简单,规划器能够理解。)

  • 约束排除期间会检查根表的所有分区上的全部约束,因此大量分区很可能显著增加查询规划时间。使用这些技术进行分区,大概在不超过一百个分区时效果较好;不要尝试使用成千上万个分区。

5.10.5. 声明式分区最佳实践 #

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

最重要的设计决策之一是选择对数据进行分区的列或者列的组合。通常最佳选择是按最常出现在分区表上执行的查询的 WHERE 子句中的列或列集合进行分区。与分区键匹配且兼容的 WHERE 子句项可用于剪枝不需要的分区。在规划分区策略时,删除不需要的数据也是需要考虑的一个因素。整个分区可以相当快地分离出去,因此把分区策略设计成让一次需要删除的所有数据都位于单个分区中,往往是有益的。

选择表应划分成多少个分区,也是一个关键决策。没有足够的分区可能意味着索引仍然太大,数据局部性仍然较差,这可能导致缓存命中率很低。但是,把表分成过多分区也会带来问题。在查询规划和执行期间,分区过多可能意味着规划时间更长、内存消耗更高。在选择如何分区时,也必须考虑将来可能发生的变化。例如,如果你选择为每个客户建立一个分区,而当前只有少量大客户,那么就应考虑几年后可能变成拥有大量小客户的情形。在这种情况下,最好选择按 RANGE 分区并选择合理数量的分区,每个分区包含固定数量的客户,而不是尝试按 LIST 进行分区,并希望客户数量的增长不会超出按数据分区的实际范围。

子分区有助于进一步拆分那些预计会比其他分区更大的分区。不过,过度使用子分区很容易导致大量分区,从而造成上一段提到的同类问题。

考虑查询计划和执行期间的分区开销也很重要。查询规划器通常能够处理多达几百个分区的层次结构。随着添加更多分区,规划时间会变长,内存消耗会更高。对于 UPDATE 和 DELETE 命令尤其如此。担心拥有大量分区的另一个原因是,服务器的内存消耗可能会随着时间的推移而显著增加,特别是如果许多会话接触大量分区。这是因为每个分区都需要将其元数据加载到接触它的每个会话的本地内存中。

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

报告文档问题

阅读 上游文档. 反馈更正前请先核对 当前版本手册.