部分索引是在表的一个子集之上建立的索引;子集由一个条件表达式定义(称为部分索引的谓词)。该索引仅为满足该谓词的表行包含条目。部分索引是一种专门的功能,但在某些情况下它们很有用。
使用部分索引的一个主要原因是为了避免索引公用值。因为查询公用值(占所有表行的一小部分)无论如何也不使用索引,因此根本没有必要在索引中保留这些行。这会缩小索引大小,从而加快确实使用索引的查询速度。由于无需在所有情况下更新索引,因此,这也可加快许多表更新操作的速度。示例 11.1显示了此理念的可能应用。
示例 11.1. 设置部分索引以排除公用值
假设你正在数据库中存储 Web 服务器访问日志。大多数访问都来自组织的 IP 地址范围,但一些来自其他位置(例如,使用拨号连接的员工)。如果按 IP 进行搜索主要是针对外部访问,则你可能无需索引对应于组织子网的 IP 范围。
假设如下所示的一张表
CREATE TABLE access_log ( url varchar, client_ip inet, ... );
要创建适合我们示例的部分索引,请使用如下命令
CREATE INDEX access_log_client_ip_ix ON access_log (client_ip) WHERE NOT (client_ip > inet '192.168.100.0' AND client_ip < inet '192.168.100.255');
可以使用此索引的典型查询将是
SELECT * FROM access_log WHERE url = '/index.html' AND client_ip = inet '212.78.10.32';
在此,查询的 IP 地址由部分索引涵盖。以下查询无法使用部分索引,因为它使用了从索引中排除的 IP 地址
SELECT * FROM access_log WHERE url = '/index.html' AND client_ip = inet '192.168.100.23';
请注意,这种类型的部分索引要求公用值预先确定,因此此类部分索引最适合用于不会更改的数据分布。此类索引可偶尔重新创建,以调整新数据分布,但这会增加维护工作量。
部分索引的另一个可能用途是从索引中排除典型查询工作负载不感兴趣的值;这在示例 11.2中有显示。这会导致与上述列出的优势相同的结果,但即使在这种情况下进行索引扫描有利可图,也不会允许通过该索引访问““无意义””值。显然,针对这种情形设置部分索引将需要特别的谨慎和大量试验。
示例 11.2. 设置部分索引以排除无意义值
如果你有一个包含已计费以及尚未计费订单的表,其中的尚未计费订单占据表的较小部分,但却是最常用的行,则可以通过对尚未计费的行建立索引来提高性能。用于创建索引的命令应当类似于以下内容
CREATE INDEX orders_unbilled_index ON orders (order_nr) WHERE billed is not true;
可以使用此索引的可能的查询如下
SELECT * FROM orders WHERE billed is not true AND order_nr < 10000;
但是,即使查询根本不涉及 order_nr
,也可以使用该索引,例如
SELECT * FROM orders WHERE billed is not true AND amount > 5000.00;
这不如在 amount
列上建立部分索引那样高效,因为系统必须扫描整个索引。但是,如果未计费的订单相对较少,则使用此部分索引仅仅是为了找到未计费的订单可能是值得的。
请注意,此查询无法使用此索引
SELECT * FROM orders WHERE order_nr = 3501;
订单 3501 可能是已计费或未计费的订单。
示例 11.2 还说明了索引列和谓词中使用的列不需要匹配。PostgreSQL 支持带有任意谓词的部分索引,只要涉及正在建立索引的表的列即可。但是,请记住,谓词必须与应当从索引中受益的查询中使用的条件匹配。准确地说,只有在系统能够识别出查询的 WHERE
条件在数学上暗示索引的谓词时,才能在查询中使用部分索引。PostgreSQL 并不具备可以识别以不同形式书写的数学等效表达式的复杂定理证明器。(不仅是这种一般的定理证明器极其难以创建,而且它可能会太慢而无法真正使用。)例如,系统可以识别简单的不等式暗示,例如 “x < 1” 暗示 “x < 2”;否则,谓词条件必须与查询的 WHERE
条件的一部分完全匹配,否则该索引才能够被识别为可用。匹配在查询计划时发生,而不在运行时发生。结果,参数化查询子句不适用于部分索引。例如,带有参数的准备查询可能指定 “x < ?”,对于参数的所有可能值,它永远不会暗示 “x < 2”。
部分索引的第三种可能用途根本不需要使用索引进行查询。这里的想法是创建表的子集上的唯一索引,如示例 11.3所示。这强制满足索引谓词的行之间唯一,而不会对不满足索引谓词的行施加限制。
示例 11.3 设置部分唯一索引
假设我们有一张描述测试结果的表。我们希望确保对于给定的主题和目标组合只有一个“successful”条目,但可能有多个“unsuccessful”条目。以下是一种实现方式
CREATE TABLE tests ( subject text, target text, success boolean, ... ); CREATE UNIQUE INDEX tests_success_constraint ON tests (subject, target) WHERE success;
在成功测试较少且不成功测试较多的情况下,这是一种特别有效的方法。还可以通过创建一个带有 IS NULL
限制的唯一部分索引,在某一列中只允许一个 null。
最后,部分索引也可以用于替代系统的查询计划选择。此外,分布特殊的数据集可能会导致系统在不应使用索引时使用它。在那种情况下,可以设置索引,使其对于有问题的查询不可用。通常情况下,PostgreSQL 会对索引使用做出合理的选定(例如,它会在检索常见值时避免使用索引,因此前面的示例实际上只节省了索引大小,无需避免使用索引),并且极不正确的计划选择是提交错误报告的原因。
请记住,设置部分索引表明你至少知道查询规划器所了解的内容,特别是你了解何时索引可能获益。形成这种知识需要经验和对 PostgreSQL 中索引工作方式的理解。在大多数情况下,部分索引相对于常规索引的优势将很小。在某些情况下,它们会产生适得其反的效果,如示例 11.4。
示例 11.4 不要将部分索引用作分区替代品
你可能会想要创建一大组不重叠的部分索引,例如
CREATE INDEX mytable_cat_1 ON mytable (data) WHERE category = 1; CREATE INDEX mytable_cat_2 ON mytable (data) WHERE category = 2; CREATE INDEX mytable_cat_3 ON mytable (data) WHERE category = 3; ... CREATE INDEX mytable_cat_N
ON mytable (data) WHERE category =N
;
这是一个坏主意!你几乎肯定可以更好地使用单个非部分索引,声明如下
CREATE INDEX mytable_cat_data ON mytable (category, data);
(出于 第 11.3 节 描述的原因,应将类别列放在首位。)虽然在比在较小的索引中搜索时可能必须在此较大的索引中下降几个树级别,但这几乎肯定比选择适当部分索引所需的计划程序努力便宜。问题的核心在于系统不了解部分索引之间的关系,并且将费力地测试每个索引以查看它是否适用于当前查询。
如果你的表足够大以至于单一索引确实是一个糟糕的主意,你应该考虑改用分区(请参见 第 5.12 节)。使用该机制后,系统确实了解表和索引是重叠的,因此可以实现更好的性能。
有关部分索引的更多信息,请参见 [ston89b]、[olson93] 和 [seshadri95]。