PostgreSQL 18:表分区

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

5.12.1 概览

分区是把逻辑上的一张大表拆成多个较小的物理部分,主要有以下好处:

  • 某些情况下能显著改善查询性能,尤其是高频访问的数据集中在一个或少数分区时。分区结构实际代替了索引树的上层部分,使常用索引部分更容易留在内存中。
  • 查询或更新访问单个分区的大部分数据时,顺序扫描该分区可能比通过索引随机读取分散在整表中的数据更快。
  • 如果设计考虑了批量装载与删除的使用方式,就可以通过增加或移除分区完成这些操作。DROP TABLE 删除分区或 ALTER TABLE DETACH PARTITION 分离分区远快于逐行处理,也完全避免了批量 DELETE 引起的 VACUUM 开销。
  • 不常使用的数据可以迁移到更便宜、速度较慢的存储介质。

通常只有表足够大时,这些优势才值得采用分区。具体阈值依应用而异,一个经验标准是表的大小超过数据库服务器的物理内存。

PostgreSQL 内置支持三种分区方式:

  • 范围分区:按照一个或多个键列的值域划分,各分区范围不重叠,下界包含、上界不包含。例如范围 1 到 10 与范围 10 到 20 相邻时,10 属于后者。日期范围、业务对象编号范围都是常见用途。
  • 列表分区:显式列出每个分区接受的键值。
  • 哈希分区:为每个分区指定模数和余数。对分区键的哈希值取模,余数匹配的行进入该分区。

需要其他划分方式时,可以采用继承或 UNION ALL 视图。这些方法更灵活,但不具备内置声明式分区的全部性能优势。

5.12.2 声明式分区

可以声明一张表由多个分区组成。这张表称为分区表,声明中包含分区方式,以及作为分区键的列或表达式。

分区表本身是没有独立存储的“虚拟”表,数据保存在与它关联的普通分区表中。每个分区按边界保存一个子集。向父分区表插入的行,会根据分区键自动路由到适当分区。更新分区键后,如果一行不再满足原分区边界,它会移至其他分区。

分区自身也可以是分区表,形成子分区。分区的列必须与父表相同,但可以拥有各自的索引、约束和默认值。详见 CREATE TABLE。

普通表与分区表不能直接互相转换。不过,可以把已有普通表或分区表附加为分区,也可以分离分区,使它重新成为独立表,从而简化维护。详见 ALTER TABLE 的 ATTACH PARTITION 与 DETACH PARTITION。

分区还可以是外部表,但用户须负责保证外部表中的内容符合分区规则,另外还存在其他限制,见 CREATE FOREIGN TABLE。

5.12.2.1 示例

假设为一家大型冰淇淋公司建立数据库,记录各地区每天的最高温度和冰淇淋销量。逻辑表如下:

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

这张表主要用于管理报表,多数查询只访问最近一周、一个月或一个季度。只保留最近三年数据,每月初删除最旧一个月的数据。分区可以同时满足这些要求。

第一步,使用 PARTITION BY 定义分区表,指定 RANGE 方式和 logdate 分区键:

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

第二步,创建分区。每个分区边界必须符合父表的分区方式和分区键;若与现有分区重叠,就会报错。分区是正常的 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 路由到它的数据,会再按 peaktemp 路由到对应子分区。直接向 measurement_y2006m02 插入也是允许的,但必须满足其分区约束,并继续路由到子分区。

子分区键可以与父分区键重合。但必须小心设计子分区边界,使它接受的数据确实是父分区范围的子集,系统不会自动验证这一点。

向父表插入没有对应分区的数据会报错,需要手动增加适当分区。分区边界约束由系统自动创建,无须重复定义。

第三步,在分区表的键列及其他需要的列上建索引。分区键索引并非强制,但通常有帮助。父表索引会自动在现有分区以及以后新建、附加的分区上创建对应索引。父表上的索引或唯一约束同样是“虚拟”的,实际数据位于各子索引中:

CREATE INDEX ON measurement (logdate);

第四步,确认 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;

这样可以在最终删除前,用 COPY、pg_dump 等工具备份,也可以先聚合、转换数据或运行报表。普通 DETACH 要求父表 ACCESS EXCLUSIVE 锁;加 CONCURRENTLY 后,父表只需要 SHARE UPDATE EXCLUSIVE 锁,但须遵守 DETACH PARTITION 的限制。

新数据可以通过创建新分区接收:

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

也可以先建独立表,在分区体系外装载、检查、转换数据,然后 ATTACH。ATTACH 在父表上只需 SHARE UPDATE EXCLUSIVE 锁,而 CREATE TABLE … PARTITION OF 需要 ACCESS EXCLUSIVE 锁,因此前者对并发更友好。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 时会扫描待附加表以验证分区约束,并在该表上持有 ACCESS EXCLUSIVE 锁。提前建立与分区边界一致的 CHECK 约束,可以避免这一扫描;附加完成后,建议删除这个冗余 CHECK。

如果待附加表本身也分区,系统会递归锁定、扫描子分区,直到发现足够的 CHECK 约束或到达叶分区。

存在 DEFAULT 分区时,建议在默认分区上提前建立排除新分区范围的 CHECK。否则系统会持有默认分区的 ACCESS EXCLUSIVE 锁并扫描,确认其中没有应属于新分区的行。默认分区自身分区时,也会递归检查。

父表索引可以自动覆盖整个分区层次,包括未来的分区,但不能直接在分区表上用 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;
...

同样方法适用于 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;
...

5.12.2.3 限制

  • 分区表上的唯一约束或主键必须包含全部分区键列,且分区键不能包含表达式或函数调用。各子索引只能直接保证本分区唯一,因此分区结构必须保证不同分区之间不会重复。
  • 排斥约束也必须包含全部分区键列,并对这些列使用相等比较,而非例如 &&。其他非分区键列可以使用任意所需比较运算符。这也是无法跨分区直接施加约束所致。
  • INSERT 的 BEFORE ROW 触发器不能改变新行最终所属分区。
  • 同一个分区树不能混合临时表与永久表。临时分区树的所有成员还必须属于同一会话。

分区在内部通过继承关联,但不能使用普通继承的所有能力。分区不能再有其他父表,一个表也不能同时继承分区表和普通表。因此声明式分区层次不会与普通表共享同一继承层次。

tableoid 和普通继承规则通常仍适用,但有以下例外:

  • 分区不能增加父表没有的列。创建分区时不能单独声明列,之后也不能 ALTER TABLE 增加列;ATTACH 时列必须与父表完全匹配。
  • 父表 CHECK 和 NOT NULL 总会继承到所有分区,不能设置为 NO INHERIT。父表仍有对应约束时,不能从分区删除它。
  • 父表没有分区时,可以用 ONLY 增删父表约束。一旦有分区,除 UNIQUE、PRIMARY KEY 外,其他约束使用 ONLY 会报错。但可以在分区本身增加约束,也可删除不来自父表的约束。
  • 父分区表本身不存储数据,因此 TRUNCATE ONLY 总会报错。

5.12.3 基于继承的分区

内置声明式分区适合大多数场景,但继承方式提供额外灵活性:子表可以有父表之外的列;支持多重继承;可以按用户自定义方式分割数据,而不限于范围、列表、哈希。不过,如果约束排除不能有效排除子表,查询可能很慢。

5.12.3.1 示例

以下建立与前述声明式方案等价的分区结构。

第一步,建立不保存数据的根表。除非约束要应用到所有子表,否则不要在根表设置 CHECK;在根表设置索引或唯一约束也不能覆盖子表,因此对这里的目标没有帮助:

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

第二步,创建继承根表的子表。通常不增加额外列。它们是正常表,也可以是外部表:

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);

第三步,给子表添加互不重叠的 CHECK,限定各自接受的键值。例如:

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

必须保证不同子表的键值范围互斥。以下写法错误,因为值 200 同时满足两个约束:

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

正确的范围写法应当类似:

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);

第四步,在每个子表的键列及其他所需列上分别建立索引:

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);

第五步,使应用对根表执行 INSERT 时,行可以路由到正确子表。可以在根表上设置触发器。若只向最新月份插入,触发器函数可以很简单:

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 会触发触发器,因此触发器方案可正常使用 COPY。规则的另一个缺点是:没有简单方式在日期未被任何规则覆盖时强制报错,数据会悄悄进入根表。

第六步,确认 postgresql.conf 未禁用 constraint_exclusion,否则可能访问不必要的子表。复杂继承层次会需要大量 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 失败;可以为子表编写更新触发器处理,但维护会复杂得多。
  • 手动 VACUUM、ANALYZE 会自动处理继承子表;使用 ONLY 可以只处理根表,例如:
ANALYZE ONLY measurement;
  • INSERT … ON CONFLICT 可能不符合预期,因为它只响应指定目标表的唯一冲突,不响应子表的冲突。
  • 除非应用自己了解分区结构,否则必须靠规则或触发器路由。触发器可能复杂,而且比声明式分区的内部行路由慢得多。

5.12.4 分区裁剪

分区裁剪是一种改善声明式分区表性能的查询优化。例如:

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

不启用裁剪时,查询会扫描每个分区。启用后,规划器检查分区边界,证明哪些分区不可能包含满足 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 参数、子查询结果、嵌套循环连接内侧的参数化值等,可能在规划时未知。执行阶段可以在以下时机裁剪:

  • 计划初始化时:使用这时已知的参数。被裁掉的分区不出现在查询的 EXPLAIN 或 EXPLAIN ANALYZE 子计划中,可以通过 Subplans Removed 看数量。但这些分区仍会在执行开始时被加锁。
  • 计划实际执行时:使用子查询或运行时参数的值。每当参与裁剪的参数改变,就可能重新裁剪。检查 EXPLAIN ANALYZE 的 loops 可了解各分区子计划实际执行次数;每次都被裁掉的子计划可能显示 (never executed)。

可以通过 enable_partition_pruning 关闭裁剪。

5.12.5 分区与约束排除

约束排除与分区裁剪类似,主要用于旧式继承分区,也能用于其他场景,包括声明式分区。

区别在于:约束排除读取每张表的 CHECK,分区裁剪读取声明式分区的边界;约束排除只在规划时生效,不在执行时删除分区。

检查 CHECK 使约束排除比裁剪慢,但也带来一种优势:声明式分区可以在内部边界之外再定义 CHECK,因此约束排除有时还能排除更多分区。

推荐且默认的 constraint_exclusion 值是 partition,而不是 on 或 off。它只对可能涉及继承分区表的查询启用该技术。on 会让规划器为全部查询检查 CHECK,即使简单查询几乎不可能受益。

注意以下限制:

  • 只在规划阶段生效。
  • WHERE 必须包含常量或外部参数。与 CURRENT_TIMESTAMP 这类非 immutable 函数比较时无法优化,因为规划器不知道执行时函数值会落在哪个分区。
  • 约束应简单。列表使用简单等值条件,范围使用简单上下界条件。经验做法是只用支持 B 树索引的运算符,将分区列与常量比较。
  • 排除时检查所有子表的所有约束,子表很多会显著增加规划时间。旧式继承分区通常适合至多约一百个子表,不宜使用成千上万个。

5.12.6 声明式分区的最佳实践

分区设计不当会同时损害规划和执行性能,因此应谨慎选择。

最关键的决定之一是分区键。通常选择最常出现在查询 WHERE 中的列或列组,有利于通过边界排除不需要的分区。但 PRIMARY KEY、UNIQUE 的要求可能迫使你采用不同键;清理数据的方式也是重要因素。整分区分离很快,因此可把需要一次删除的数据集中在同一分区。

分区数量同样关键。过少时,索引仍然很大,数据局部性差,缓存命中率低;过多时,规划时间和规划、执行的内存消耗都可能增加。

还要考虑未来变化。例如,当前只有少数大客户,按每个客户一个分区似乎合理;几年后可能变成大量小客户。这种情况可考虑固定合理分区数量的 HASH,而不是按 LIST 为每个客户建分区并寄希望于客户数量不增长。

子分区可进一步拆分预计会特别大的分区,多列范围分区也是一种选择。但两种方案都容易造成分区数过多,需保持克制。

如果典型查询能裁掉绝大多数分区,规划器通常能较好地处理几千个分区。裁剪后剩余分区越多,规划时间和内存消耗越高。大量会话访问大量分区时,服务器内存还可能随时间显著增长,因为每个会话都要把访问分区的元数据加载到自己的本地内存。

数据仓库的执行时间通常占主要部分,因此相较 OLTP 可以容忍更多分区和较长规划时间。两类负载都应尽早做对设计,因为重新划分海量数据非常缓慢。

用目标工作负载做模拟通常有助于选择方案。不要假定分区一定越多越好,也不要假定越少越好。

来源与许可

原文:PostgreSQL 18 文档 5.12 Table Partitioning。注册来源为 current 页面,本稿核对时该页显示版本 18。Copyright © 1996–2026 The PostgreSQL Global Development Group。本文为中文翻译,示例和查询计划保留原文,未在本环境执行。

本内容按 PostgreSQL License 使用,其许可证允许使用、复制、修改和分发软件及文档;完整声明随交接来源保留。

PostgreSQL Database Management System
(also known as Postgres, formerly as Postgres95)

Portions Copyright © 1996-2026, The PostgreSQL Global Development Group
Portions Copyright © 1994, The Regents of the University of California

Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies.

IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容