# 11.8.部分索引

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

使用部分索引的一个主要原因是避免索引公共值。由于搜索公共值(占所有表行数百分之几以上的值)的查询无论如何都不会使用索引,因此将这些行保留在索引中毫无意义。这会减少索引的大小,从而加快使用索引的查询的速度。它还将加快许多表更新操作,因为在所有情况下都不需要更新索引。例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;

但是,该索引也可以用于不涉及订单号例如:

SELECT * FROM orders WHERE billed is not true AND amount > 5000.00;

这比不上数据库上的部分索引数量列将是,因为系统必须扫描整个索引。然而,如果有相对较少的未开票订单,那么使用这个部分指数仅仅查找未开票订单可能是一个胜利。

请注意,此查询无法使用此索引:

SELECT * FROM orders WHERE order_nr = 3501;

3501号订单可能属于已开票或未开票订单。

例11.2还说明了索引列和谓词中使用的列不需要匹配。PostgreSQL支持带有任意谓词的部分索引,只要只涉及被索引表的列。但是,请记住,谓词必须与查询中使用的条件相匹配,这些条件本应受益于索引。准确地说,只有当系统能够识别哪里查询的条件在数学上暗示了索引的谓词。PostgreSQL没有一个复杂的定理证明程序,可以识别以不同形式编写的数学等价表达式。(创建这样一个通用的定理证明程序不仅极其困难,而且速度可能太慢,无法真正发挥作用。)该系统可以识别简单的不等式含义,例如“x\<1”意味着“x\<2”;否则,谓词条件必须与查询条件的一部分完全匹配哪里条件或索引将不会被识别为可用。匹配在查询计划时进行,而不是在运行时。因此,参数化查询子句不能与部分索引一起使用。例如,带有参数的准备好的查询可能会指定“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;

当成功的测试很少,失败的测试很多时,这是一种特别有效的方法。通过创建一个具有是空的限制规定

最后,还可以使用部分索引覆盖系统的查询计划选择。此外,具有特殊分布的数据集可能会导致系统在实际上不应该使用索引的情况下使用索引。在这种情况下,可以设置索引,使其不可用于有问题的查询。通常情况下,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.11节).有了这种机制,系统就明白了表和索引是不重叠的,因此可以获得更好的性能。

有关部分索引的更多信息,请参见[89B], [olson93][seshadri95].