PostgreSQL 支持基本表分区。本节介绍为什么以及如何将分区作为数据库设计的一部分来实现。
分区是指将逻辑上一个大表拆分成较小的物理部分。分区可以提供以下几个好处:
在某些情况下,查询性能可以得到显著提高,特别是当表的大部分频繁访问的行位于单个分区或少数几个分区中时。分区有效地替代了索引的较高树级别,使索引的常用部分更有可能适合内存。
当查询或更新访问单个分区的很大一部分时,可以通过对该分区进行顺序扫描来提高性能,而不是使用索引,因为索引需要在整个表中进行随机读取。
如果在使用模式中考虑了分区设计,则可以通过添加或删除分区来完成批量加载和删除。使用 DROP TABLE
删除单个分区,或者执行 ALTER TABLE DETACH PARTITION
,比批量操作快得多。这些命令还完全避免了批量 DELETE
引起的 VACUUM
开销。
不常用的数据可以迁移到更便宜和更慢的存储介质。
只有当表原本非常大时,这些好处通常才值得。表从分区中受益的确切时间点取决于应用程序,不过一个经验法则是表的大小应该超过数据库服务器的物理内存。
PostgreSQL 为以下形式的分区提供内置支持:
如果您的应用程序需要使用上面未列出的其他形式的分区,则可以使用继承和 UNION ALL
视图等替代方法。这些方法提供了灵活性,但没有内置声明式分区的某些性能优势。
PostgreSQL 允许您声明一个表被划分为多个分区。被划分的表称为分区表。该声明包括如上所述的分区方法,以及要用作分区键的列或表达式的列表。
分区表本身是一个“虚拟”表,没有自己的存储。相反,存储属于分区,这些分区是与分区表关联的普通表。每个分区根据其分区边界定义的存储数据子集。插入到分区表中的所有行都将根据分区键列的值路由到相应的分区。更新行的分区键将导致该行移动到不同的分区(如果它不再满足其原始分区的分区边界)。
分区本身可以定义为分区表,从而产生子分区。尽管所有分区都必须具有与其分区父级相同的列,但分区可能具有自己的索引、约束和默认值,这些值与其他分区不同。有关创建分区表和分区的更多详细信息,请参阅CREATE TABLE。
不可能将常规表转换为分区表,反之亦然。但是,可以将现有的常规表或分区表添加为分区表的分区,或者从分区表中删除分区,将其转换为独立表;这可以简化和加快许多维护过程。有关 ATTACH PARTITION
和 DETACH PARTITION
子命令的更多信息,请参阅ALTER TABLE。
分区也可以是外部表,但需要格外小心,因为此时用户有责任确保外部表的内容满足分区规则。还有一些其他限制。有关更多信息,请参阅CREATE FOREIGN TABLE。
假设我们正在为一家大型冰淇淋公司构建数据库。该公司每天测量每个地区的最高温度以及冰淇淋销量。从概念上讲,我们需要一个类似这样的表:
CREATE TABLE measurement ( city_id int not null, logdate date not null, peaktemp int, unitsales int );
我们知道大多数查询只会访问最近一周、一个月或一个季度的数据,因为此表的主要用途是为管理层准备在线报告。为了减少需要存储的旧数据量,我们决定仅保留最近 3 年的数据。在每个月初,我们将删除最旧的一个月的数据。在这种情况下,我们可以使用分区来帮助我们满足对 measurements 表的所有不同要求。
在这种情况下,要使用声明式分区,请使用以下步骤:
通过指定 PARTITION BY
子句将 measurement
表创建为分区表,该子句包括分区方法(在这种情况下为 RANGE
)和用作分区键的列列表。
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
表中的、映射到 measurement_y2006m02
的数据(或者直接插入到 measurement_y2006m02
的数据,如果满足其分区约束也是允许的),都会根据 peaktemp
列进一步重定向到其分区之一。指定的分区键可能与父表的分区键重叠,但应注意指定子分区的边界,使其接受的数据集构成该分区自身边界允许的数据集的子集;系统不会尝试检查是否确实如此。
将数据插入到父表中,但该数据没有映射到任何现有分区,将会导致错误;必须手动添加适当的分区。
不必手动创建描述分区边界条件的表约束。这些约束将自动创建。
在分区表上创建键列的索引,以及您可能需要的任何其他索引。(键索引不是绝对必要的,但在大多数情况下它很有用。)这会自动在每个分区上创建匹配的索引,并且您以后创建或附加的任何分区也将具有此类索引。在分区表上声明的索引或唯一约束与分区表一样是“虚拟的”:实际数据位于各个分区表上的子索引中。
CREATE INDEX ON measurement (logdate);
确保在 postgresql.conf
中未禁用 enable_partition_pruning 配置参数。 如果禁用,则查询将不会按预期进行优化。
在上面的示例中,我们将每月创建一个新分区,因此编写一个自动生成所需 DDL 的脚本可能是明智的。
通常,最初定义表时建立的分区集并不打算保持静态。通常需要删除包含旧数据的分区,并定期为新数据添加新分区。分区最重要的优势之一恰恰在于,它允许通过操作分区结构来几乎立即执行此项原本很麻烦的任务,而不是物理地移动大量数据。
删除旧数据的最简单方法是删除不再需要的分区
DROP TABLE measurement_y2006m02;
这可以非常快速地删除数百万条记录,因为它不必单独删除每条记录。但请注意,上述命令需要在父表上获取 ACCESS EXCLUSIVE
锁。
另一种通常更可取的选择是从分区表中删除分区,但保留对其作为独立表的访问权限。这有两种形式
ALTER TABLE measurement DETACH PARTITION measurement_y2006m02; ALTER TABLE measurement DETACH PARTITION measurement_y2006m02 CONCURRENTLY;
这些操作允许在删除数据之前对数据执行进一步的操作。例如,这通常是使用 COPY
、pg_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; ...
此技术也可以用于 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; ...
以下限制适用于分区表
要在分区表上创建唯一约束或主键约束,分区键不得包含任何表达式或函数调用,并且约束的列必须包含所有分区键列。存在此限制的原因是组成约束的各个索引只能在其自己的分区内直接强制执行唯一性;因此,分区结构本身必须保证不同分区中没有重复项。
类似地,排除约束必须包括所有分区键列。此外,约束必须比较这些列是否相等(例如,而不是 &&
)。同样,此限制源于无法强制执行跨分区限制。约束可以包含不是分区键的其他列,并且可以使用您喜欢的任何运算符比较这些列。
BEFORE ROW
INSERT
上的触发器无法更改新行的最终目标分区。
不允许在同一分区树中混合临时关系和永久关系。因此,如果分区表是永久的,则其分区也必须是永久的,反之亦然。使用临时关系时,分区树的所有成员都必须来自同一会话。
各个分区在后台使用继承链接到其分区表。但是,如下所述,不可能将继承的所有通用功能与声明性分区表或其分区一起使用。值得注意的是,分区不能有除其所属分区表之外的任何父级,表也不能同时从分区表和常规表继承。这意味着分区表及其分区永远不会与常规表共享继承层次结构。
由于由分区表及其分区组成的分区层次结构仍然是继承层次结构,因此 tableoid
和继承的所有常规规则都适用,如第 5.11 节中所述,但有一些例外
分区不能具有父表中不存在的列。使用 CREATE TABLE
创建分区时,无法指定列,也无法使用 ALTER TABLE
在事后向分区添加列。只有当表的列与父表的列完全匹配时,才能使用 ALTER TABLE ... ATTACH PARTITION
将表添加为分区。
分区表的 CHECK
和 NOT NULL
约束始终由其所有分区继承。不允许在分区表上创建标记为 NO INHERIT
的 CHECK
约束。如果父表中存在相同的约束,则不能删除分区列上的 NOT NULL
约束。
只要没有分区,支持使用 ONLY
来仅在分区表上添加或删除约束。一旦存在分区,对于除 UNIQUE
和 PRIMARY KEY
之外的任何约束,使用 ONLY
将导致错误。相反,可以在分区本身上添加约束,并且(如果它们不存在于父表中)可以删除约束。
由于分区表本身不包含任何数据,尝试在分区表上使用 TRUNCATE
ONLY
总是会返回错误。
虽然内置的声明式分区适用于大多数常见用例,但在某些情况下,更灵活的方法可能更有用。可以使用表继承来实现分区,这允许声明式分区不支持的几个功能,例如
对于声明式分区,分区必须与分区表具有完全相同的列集,而使用表继承,子表可以具有父表中不存在的额外列。
表继承允许进行多重继承。
声明式分区仅支持范围、列表和哈希分区,而表继承允许以用户选择的方式划分数据。(但是,请注意,如果约束排除无法有效地修剪子表,查询性能可能会很差。)
此示例构建与上述声明式分区示例等效的分区结构。请使用以下步骤
创建 “根” 表,所有 “子” 表都将从该表继承。此表将不包含任何数据。除非您打算将检查约束平等地应用于所有子表,否则不要在此表上定义任何检查约束。在其上定义任何索引或唯一约束也没有意义。在我们的示例中,根表是最初定义的 measurement
表
CREATE TABLE measurement ( city_id int not null, logdate date not null, peaktemp int, unitsales int );
创建几个从根表继承的 “子” 表。通常,这些表不会在从根表继承的列集中添加任何列。与声明式分区一样,这些表在各方面都是正常的 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);
向子表添加不重叠的表约束,以定义每个子表中允许的键值。
典型的例子如下:
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);
对于每个子表,在键列以及您可能需要的任何其他索引上创建索引。
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 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
会触发触发器,因此如果您使用触发器方法,则可以正常使用它。
规则方法的另一个缺点是,如果规则集不涵盖插入日期,则没有简单的方法强制出错;数据将静默地进入根表。
确保在 postgresql.conf
中未禁用 constraint_exclusion 配置参数;否则可能会不必要地访问子表。
正如我们所看到的,复杂的表层次结构可能需要大量的 DDL。在上面的例子中,我们将每月创建一个新的子表,因此编写一个自动生成所需 DDL 的脚本可能是明智的。
要快速删除旧数据,只需删除不再需要的子表即可
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;
以下注意事项适用于使用继承实现的分区
没有自动方法来验证所有 CHECK
约束是否互斥。创建生成子表并创建和/或修改关联对象的代码比手动编写每个子表更安全。
索引和外键约束适用于单个表,而不适用于它们的继承子表,因此它们有一些 注意事项 需要注意。
此处显示的方案假设行的键列的值永远不会更改,或者至少不会更改到需要将其移动到另一个分区的程度。尝试执行此操作的 UPDATE
将因 CHECK
约束而失败。如果您需要处理这种情况,可以在子表上放置合适的更新触发器,但这会使结构的维护复杂得多。
如果您使用的是手动 VACUUM
或 ANALYZE
命令,请不要忘记您需要在每个子表上单独运行它们。像这样的命令
ANALYZE measurement;
将仅处理根表。
带有 ON CONFLICT
子句的 INSERT
语句不太可能按预期工作,因为只有在指定目标关系而不是其子关系上发生唯一性冲突时,才会执行 ON CONFLICT
操作。
除非应用程序明确了解分区方案,否则将需要触发器或规则来将行路由到所需的子表。触发器可能很难编写,并且会比声明式分区内部执行的元组路由慢得多。
分区修剪是一种查询优化技术,可提高声明式分区表的性能。例如:
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
语句中定义的参数、使用从子查询获得的值或在嵌套循环连接的内侧使用参数化值时,它可以允许修剪更多分区。可以在以下任何时间执行执行期间的分区修剪
在查询计划初始化期间。可以在此处对执行的初始化阶段期间已知的参数值执行分区修剪。在此阶段修剪的分区将不会显示在查询的 EXPLAIN
或 EXPLAIN ANALYZE
中。可以通过观察 EXPLAIN
输出中的 “Subplans Removed” 属性来确定在此阶段删除的分区数量。
在实际执行查询计划期间,也可能执行分区裁剪,以删除那些仅在实际查询执行期间才知道值的分区。这包括来自子查询的值以及来自执行时参数的值,例如参数化嵌套循环连接中的参数。由于这些参数的值在查询执行期间可能会多次更改,因此每当用于分区裁剪的执行参数之一发生更改时,都会执行分区裁剪。要确定在此阶段是否裁剪了分区,需要仔细检查 EXPLAIN ANALYZE
输出中的 loops
属性。对应于不同分区的子计划可能具有不同的值,具体取决于在执行期间每个子计划被裁剪的次数。如果每次都被裁剪,则某些子计划可能会显示为 (never executed)
。
可以使用 enable_partition_pruning 设置禁用分区裁剪。
约束排除 是一种类似于分区裁剪的查询优化技术。虽然它主要用于使用传统的继承方法实现的分区,但它也可以用于其他目的,包括声明式分区。
约束排除的工作方式与分区裁剪非常相似,不同之处在于它使用每个表的 CHECK
约束(这也是它名称的由来),而分区裁剪使用表的仅在声明式分区情况下才存在的分区边界。另一个区别是,约束排除仅在计划时应用;不会尝试在执行时删除分区。
约束排除使用 CHECK
约束的事实使其比分区裁剪慢,但这有时可以作为优势:因为除了内部的分区边界之外,甚至可以在声明式分区表上定义约束,约束排除可能能够从查询计划中省略其他分区。
constraint_exclusion 的默认(也是推荐的)设置既不是 on
也不是 off
,而是一个名为 partition
的中间设置,它会使该技术仅应用于可能正在处理继承分区表的查询。on
设置会导致规划器检查所有查询中的 CHECK
约束,即使是那些不太可能受益的简单查询。
以下注意事项适用于约束排除:
约束排除仅在查询规划期间应用,与分区裁剪不同,分区裁剪也可以在查询执行期间应用。
约束排除仅在查询的 WHERE
子句包含常量(或外部提供的参数)时才起作用。例如,与非不可变函数(例如 CURRENT_TIMESTAMP
)的比较无法优化,因为规划器无法知道该函数的值在运行时可能属于哪个子表。
保持分区约束简单,否则规划器可能无法证明不需要访问子表。对于列表分区,请使用简单的相等条件,对于范围分区,请使用简单的范围测试,如前面的示例所示。一个好的经验法则是,分区约束应仅包含分区列与使用 B 树索引运算符的常量的比较,因为只允许在分区键中使用 B 树索引列。
在约束排除期间会检查父表的所有子表的所有约束,因此大量的子表可能会大大增加查询规划时间。因此,基于传统继承的分区最多可以处理大约一百个子表;不要尝试使用数千个子表。
应该仔细选择如何对表进行分区,因为查询计划和执行的性能可能会因设计不佳而受到负面影响。
最关键的设计决策之一将是您用于分区数据的列或列集。通常,最好的选择是按最常出现在对分区表执行的查询的 WHERE
子句中的列或列集进行分区。与分区边界约束兼容的 WHERE
子句可用于裁剪不需要的分区。但是,您可能被迫根据 PRIMARY KEY
或 UNIQUE
约束的要求做出其他决定。在规划分区策略时,删除不需要的数据也是一个需要考虑的因素。可以相当快地分离整个分区,因此以一种使所有要一次删除的数据都位于单个分区中的方式设计分区策略可能是有益的。
选择表应划分为的目标分区数也是需要做出的关键决定。没有足够的分区可能意味着索引仍然太大,并且数据局部性仍然很差,这可能导致低缓存命中率。但是,将表分成太多分区也会导致问题。分区过多可能意味着更长的查询计划时间和在查询计划和执行期间更高的内存消耗,如下所述。在选择如何对表进行分区时,还必须考虑将来可能发生的变化。例如,如果您选择为每个客户设置一个分区,而您目前只有少量的大客户,请考虑如果几年后您发现自己拥有大量的小客户,会产生什么影响。在这种情况下,最好选择按 HASH
进行分区并选择合理数量的分区,而不是尝试按 LIST
进行分区并希望客户数量不会增加到超出实际数据分区范围。
子分区可用于进一步划分预计会比其他分区更大的分区。另一种选择是在分区键中使用多个列进行范围分区。这两种方法都可能很容易导致过多的分区,因此建议克制。
重要的是要考虑在查询计划和执行期间的分区开销。查询规划器通常能够很好地处理具有多达数千个分区的分区层次结构,前提是典型的查询允许查询规划器裁剪除少量分区之外的所有分区。当规划器执行分区裁剪后,保留的分区越多,规划时间就越长,内存消耗就越高。担心拥有大量分区的另一个原因是服务器的内存消耗可能会随着时间的推移而显着增长,尤其是在许多会话接触大量分区时。这是因为每个分区都需要将其元数据加载到每个访问它的会话的本地内存中。
对于数据仓库类型的工作负载,使用比以下更多数量的分区是有意义的:OLTP类型的工作负载。通常,在数据仓库中,查询计划时间不是那么重要,因为大部分处理时间都花费在查询执行期间。对于这两种类型的工作负载,尽早做出正确的决策非常重要,因为重新分区大量数据可能非常缓慢。对预期工作负载进行模拟通常有利于优化分区策略。切勿仅假设分区越多越好,反之亦然。
如果您在文档中发现任何不正确的内容,或者与您对特定功能的体验不符,或者需要进一步澄清,请使用此表格报告文档问题。