受支持版本: 当前版本 (18) / 17 / 16 / 15 / 14
开发版本: devel

11.8. 部分索引 #

部分索引是建立在表的一个子集上的索引;这个子集由一个条件表达式定义(称为部分索引的谓词)。索引中只包含满足该谓词的表行的项。部分索引是一种专门特性,但有几种场景下它会很有用。

使用部分索引的一个主要原因是避免索引常见值。由于搜索常见值的查询(即占全部表行百分之几以上的值)反正也不会使用索引,因此完全没有必要把这些行保留在索引里。这会减小索引尺寸,从而加快那些确实会使用该索引的查询。它也会加快很多表更新操作,因为索引并不需要在所有情况下都更新。Example 11.1展示了这种思路的一种可能应用。

Example 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';

请注意,这类部分索引要求常见值事先可知,因此最适合用于数据分布不会变化的场景。也可以偶尔重建这类索引以适应新的数据分布,但那会增加维护工作量。


部分索引的另一种可能用途,是把典型查询工作负载不感兴趣的值排除在索引之外,如Example 11.2所示。这样会得到与上面相同的好处,但也意味着这些不感兴趣的值无法通过该索引访问,即使在那种情况下索引扫描可能是有利的。显然,为这种场景建立部分索引需要大量的谨慎和实验。

Example 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条件在数学上蕴含该索引的谓词时,部分索引才能用于该查询。PostgreSQL并没有一个复杂的定理证明器,来识别那些写法不同但数学上等价的表达式。(不仅构建这样一个通用定理证明器极其困难,而且它很可能也会慢到失去实际用途。)系统可以识别简单的不等式蕴含,例如x < 1蕴含x < 2;否则,谓词条件必须与查询WHERE条件的某一部分完全匹配,否则索引不会被识别为可用。匹配发生在查询规划阶段,而不是运行时。因此,参数化查询子句无法与部分索引配合工作。例如,一个带参数的预备查询可能写成x < ?,而对于参数的所有可能取值,它都不可能蕴含x < 2

部分索引的第三种可能用途,甚至不要求索引被查询使用。这里的思路是像Example 11.3那样,在表的一个子集上创建唯一索引。这样就能在满足索引谓词的那些行之间强制唯一性,而不会约束不满足谓词的行。

Example 11.3. 建立一个部分唯一索引

假设我们有一张描述测试结果的表。我们希望确保对于给定的subjecttarget组合,只有一条成功记录,但可以有任意多条失败记录。实现方法之一如下:

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

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

当成功测试很少而失败测试很多时,这是一种特别高效的方法。也可以通过创建一个带IS NULL限制的唯一部分索引,来让某一列只允许出现一个空值。


最后,部分索引还可以用来影响系统的查询计划选择。同样,分布异常的数据集可能导致系统在实际上不应该使用索引的时候选择使用它。在这种情况下,可以把索引建成对那个有问题的查询不可用。通常,PostgreSQL会对索引使用作出合理选择(例如,它会在检索常见值时避免使用索引,因此前面的例子实际上只是节省索引空间,而不是为了避免使用索引所必需的),如果出现明显错误的计划选择,那就应当提交 bug 报告。

请记住,建立部分索引意味着你至少与查询规划器一样了解情况,尤其是你知道索引何时可能是有利的。形成这种认识需要经验,以及对PostgreSQL中索引工作方式的理解。在大多数情况下,部分索引相比普通索引的优势都很小。有些场景下它们甚至会适得其反,如Example 11.4所示。

Example 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);

(把 category 列放在前面,原因见Section 11.3。)虽然在这个较大的索引中搜索,可能要比在较小索引中搜索多下降几层树,但这几乎肯定仍然比规划器为了从这些部分索引中选出合适的一个所付出的代价更低。问题的核心在于,系统并不了解这些部分索引之间的关系,因此会费力地逐个测试它们,看它们是否适用于当前查询。

如果你的表大到单个索引确实不是好主意,那么你应该考虑改用分区(见Section 5.12)。借助这种机制,系统能够理解这些表和索引彼此不重叠,因此有可能获得好得多的性能。


关于部分索引的更多信息可参见[ston89b][olson93][seshadri95]

提交更正

如果您发现文档中有不正确的内容、与您使用特定功能的经验不符或需要进一步说明,请使用此表单来报告文档问题。