一个部分索引是建立在一张表的一部分上的索引;这部分数据由一个条件表达式(称为部分索引的谓词)定义。该索引只包含满足谓词的表行条目。部分索引是一项特殊功能,但在许多情况下都很有用。
使用部分索引的一个主要原因是避免对常用值进行索引。由于查询常用值(占表中行数百分比很高的值)时不会使用索引,因此完全没有必要将这些行保留在索引中。这会减小索引的大小,从而加快那些确实使用了索引的查询的速度。同时,它还会加快许多表更新操作的速度,因为在所有情况下都不需要更新索引。示例 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
约束的唯一部分索引,也可以只允许一列为 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]。
如果您在文档中发现任何不正确之处、与您对特定功能的实际经验不符之处或需要进一步阐明之处,请使用此表格报告文档问题。