2024年9月26日: PostgreSQL 17 发布!
支持版本:当前 (17) / 16 / 15 / 14 / 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 / 8.0 / 7.4 / 7.3 / 7.2

11.8. 部分索引 #

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

使用部分索引的一个主要原因是避免对常见值进行索引。由于搜索常见值(占所有表行百分比超过几个百分点的值)的查询无论如何都不会使用索引,因此完全没有必要将这些行保留在索引中。这减少了索引的大小,从而加快了确实使用索引的查询的速度。它还会加快许多表更新操作的速度,因为不需要在所有情况下都更新索引。示例 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. 设置一个局部唯一索引

假设我们有一个描述测试结果的表。我们希望确保对于给定的主题和目标组合,只有一个“成功”条目,但可能存在任意数量的“失败”条目。以下是一种方法

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

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

当成功测试很少而失败测试很多时,这是一种特别有效的方法。还可以通过使用IS 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]

提交更正

如果您在文档中看到任何不正确的内容,与您对特定功能的体验不符,或者需要进一步澄清,请使用此表单 报告文档问题。