阅读(4355) (23)

PostgreSQL 部分索引

2021-08-17 15:08:00 更新

一个部分索引是建立在表的一个子集上,而该子集则由一个条件表达式(被称为部分索引的谓词)定义。而索引中只包含那些符合该谓词的表行的项。部分索引是一种专门的特性,但在很多种情况下它们也很有用。

使用部分索引的一个主要原因是避免索引公值。由于搜索一个公值的查询(一个在所有表行中占比超过一定百分比的值)不会使用索引,所以完全没有理由将这些行保留在索引中。这可以减小索引的尺寸,同时也将加速使用索引的查询。它也将加速很多表更新操作,因为这种索引并不需要在所有情况下都被更新。例 11.1展示了一种可能的应用:

例 11.1. 建立一个部分索引来排除公值

假设我们要在一个数据库中保存网页服务器访问日志。大部分访问都来自于我们组织内的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.11 节)。 通过这种机制,系统理解表和索引是不重叠的,就此而言可以获得更好的性能。


关于部分索引的更多信息可以在[ston89b][olson93][seshadri95]中找到。