11.8. 部分索引

部分索引是构建在表子集上的索引;子集由条件表达式(称为部分索引的谓词)定义。索引仅包含满足谓词的那些表行的条目。部分索引是一种特殊功能,但是在某些情况下它们很有用。

使用部分索引的一个主要原因是避免索引公共值。由于搜索公共值(占所有表行的百分之几以上)的查询将始终不使用索引,因此完全没有必要将这些行保留在索引中。这样可以减小索引的大小,从而可以加快那些确实使用索引的查询的速度。因为不需要在所有情况下都更新索引,所以它也将加快许多表的更新操作。 Example 11.1显示了此想法的可能应用。

示例 11.1. 设置部分索引以排除公共值

假设您正在将 Web 服务器访问日志存储在数据库中。大多数访问源自组织的 IP 地址范围,但某些访问则来自其他地方(例如,拨号 Connecting 的员工)。如果按 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';

无法使用此索引的查询是:

SELECT *
FROM access_log
WHERE client_ip = inet '192.168.100.23';

请注意,这种部分索引要求预先确定公共值,因此,此类部分索引最适合用于不变的数据分发。可以偶尔重新创建索引以适应新的数据分布,但这会增加维护工作。

部分索引的另一种可能用法是从索引中排除典型查询工作负载不感兴趣的值;如Example 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 可能在已开票或未开票的订单中。

Example 11.2还说明了索引列和谓词中使用的列不需要匹配。 PostgreSQL 支持带有任意谓词的部分索引,只要只涉及要索引的表的列即可。但是,请记住,谓词必须与应该从索引中受益的查询中使用的条件匹配。确切地说,只有系统可以识别出查询的WHERE条件在 math 上暗示了索引的谓词时,才可以在查询中使用部分索引。 PostgreSQL 没有完善的定理证明器,可以识别以不同形式编写的 math 等效表达式。 (这样的通用定理证明者不仅很难创建,而且可能太慢而无法实际使用.)系统可以识别简单的不等式,例如“ x <1”意味着“ x <2”;否则,谓词条件必须与查询WHERE条件的一部分完全匹配,否则索引将不会被识别为可用。匹配发生在查询计划时,而不是运行时。结果,参数化查询子句不适用于部分索引。例如,带有参数的准备好的查询可能指定“ x <?”对于参数的所有可能值,它永远都不会暗示“ x <2”。

部分索引的第三种可能用法根本不需要在查询中使用索引。这里的想法是在表的子集上创建唯一索引,如Example 11.3。这将在满足索引谓词的行之间强制唯一性,而不会限制不满足条件的行。

例 11.3 设置部分唯一索引

假设我们有一张描述测试结果的表格。我们希望确保给定的主题和目标组合只有一个“成功”条目,但是可能有任意数量的“不成功”条目。这是一种实现方法:

CREATE TABLE tests (
    subject text,
    target text,
    success boolean,
    ...
);

CREATE UNIQUE INDEX tests_success_constraint ON tests (subject, target)
    WHERE success;

当很少有成功的测试而很多没有成功的测试时,这是一种特别有效的方法。

通过使用部分索引子句仅处理空列值,并使用表达式 index 子句对true而不是null进行索引,此索引仅允许索引列中的一个空值:

CREATE UNIQUE INDEX tests_target_one_null ON tests ((target IS NULL)) WHERE target IS NULL;

最后,部分索引还可以用于覆盖系统的查询计划选择。同样,具有特殊分布的数据集可能会导致系统在实际上不应使用索引时使用它。在那种情况下,可以设置索引,以使索引不适用于有问题的查询。通常,PostgreSQL 对索引使用情况做出合理的选择(例如,在检索公共值时避免使用它们,因此前面的示例实际上仅节省了索引大小,不需要避免使用索引),并且错误选择计划是导致错误的原因报告。

请记住,设置部分索引表示您至少了解查询计划者所了解的知识,尤其是您知道何时索引可以获利。形成这些知识需要经验和对 PostgreSQL 中索引如何工作的理解。在大多数情况下,部分索引优于常规索引的优势将很小。在某些情况下,它们会适得其反,例如Example 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);

(出于Section 11.3中所述的原因,将类别列放在第一位。)尽管在较大索引中的搜索可能比在较小索引中的搜索要经过更多的树级别,但几乎可以肯定,这比计划程序便宜选择适当的部分索引之一需要付出的努力。问题的核心是系统无法理解部分索引之间的关系,因此会费力地测试每个索引以查看其是否适用于当前查询。

如果您的表足够大,以至于单个索引确实不是一个好主意,则应考虑使用分区(请参见Section 5.10)。通过这种机制,系统确实知道表和索引是不重叠的,因此可能会有更好的性能。

有关局部索引的更多信息,请参见[ston89b][olson93][seshadri95]