# 11.9.仅索引扫描和覆盖索引

PostgreSQL中的所有索引都是次要的索引,这意味着每个索引都与表的主数据区(称为表的主数据区)分开存储用PostgreSQL术语)。这意味着在普通索引扫描中,每一行检索都需要从索引和堆中获取数据。此外,与给定索引项匹配的索引项哪里条件通常在索引中靠得很近,它们引用的表行可能在堆中的任何位置。因此,索引扫描的堆访问部分涉及大量对堆的随机访问,这可能会很慢,尤其是在传统的旋转介质上。(如中所述)第11.5节,位图扫描试图通过按排序顺序进行堆访问来降低成本,但这仅限于此。)

为了解决这个性能问题,PostgreSQL支持仅索引扫描,它可以单独回答来自索引的查询,而无需任何堆访问。基本思想是直接从每个索引项返回值,而不是查询相关的堆项。使用此方法的时间有两个基本限制:

  1. 索引类型必须支持仅索引扫描。B树索引总是这样。GiST和SP GiST索引只支持索引扫描某些运算符类,而不支持其他运算符类。其他索引类型不受支持。基本要求是索引必须物理存储或重建每个索引项的原始数据值。作为反例,GIN索引不能支持仅索引扫描,因为每个索引项通常只包含原始数据值的一部分。

  2. 查询必须只引用存储在索引中的列。例如,给定列上的索引十、y也有一列的表的z,这些查询只能使用索引扫描:

    SELECT x, y FROM tab WHERE x = 'key';
    SELECT x FROM tab WHERE x = 'key' AND y < 42;
    

    但这些问题不能:

    SELECT x, z FROM tab WHERE x = 'key';
    SELECT x FROM tab WHERE x = 'key' AND z < 42;
    

    (表达式索引和部分索引使该规则复杂化,如下所述。)

    如果这两个基本要求都得到满足,那么查询所需的所有数据值都可以从索引中获得,因此仅索引扫描在物理上是可能的。但是PostgreSQL中的任何表扫描都有一个额外的要求:它必须验证每个检索到的行对查询的MVCC快照“可见”,如中所述第13章.可见性信息不存储在索引项中,只存储在堆项中;所以乍一看,似乎每一行检索都需要一个堆访问。如果最近修改了表行,情况确实如此。然而,对于很少变化的数据,有一种方法可以解决这个问题。对于表堆中的每个页面,PostgreSQL都会跟踪该页面中存储的所有行是否足够旧,以便所有当前和将来的事务都可以看到。这些信息存储在表的了望台联络图。仅索引扫描在找到候选索引项后,检查相应堆页面的可见性映射位。如果设置了,则该行已知可见,因此可以返回数据,而无需进行进一步的工作。如果未设置,则必须访问堆条目以确定其是否可见,因此与标准索引扫描相比,不会获得性能优势。即使在成功的情况下,这种方法也会用可见性映射访问交换堆访问;但由于可见性映射比它描述的堆小四个数量级,因此访问它所需的物理I/O要少得多。在大多数情况下,可见性映射始终缓存在内存中。

    简而言之,虽然考虑到两个基本要求,仅索引扫描是可能的,但只有当表的大部分堆页面设置了它们的所有可见映射位时,它才会是一个胜利。但是大部分行不变的表很常见,以至于这种类型的扫描在实践中非常有用。

为了有效地使用仅索引扫描功能,您可以选择创建一个覆盖指数,这是一个专门设计用于包含您经常运行的特定类型查询所需的列的索引。由于查询通常需要检索的列不仅仅是它们搜索的列,因此 PostgreSQL 允许您创建一个索引,其中某些列只是“有效负载”而不是搜索键的一部分。这是通过添加一个包括列出额外列的子句。例如,如果您经常运行如下查询

SELECT y FROM tab WHERE x = 'key';

加速此类查询的传统方法是在x只要。但是,索引定义为

CREATE INDEX tab_x_y ON tab(x) INCLUDE (y);

可以将这些查询作为仅索引扫描处理,因为是的无需访问堆即可从索引中获取。

因为列是的不是索引搜索键的一部分,它不必是索引可以处理的数据类型;它仅存储在索引中,不被索引机制解释。此外,如果索引是唯一索引,即

CREATE UNIQUE INDEX tab_x_y ON tab(x) INCLUDE (y);

唯一性条件仅适用于列x,而不是结合x是的.(一个包括子句也可以写成独特首要的关键约束,为设置这样的索引提供替代语法。)

将非键有效负载列添加到索引,尤其是宽列时,谨慎行事是明智之举。如果索引元组超过索引类型允许的最大大小,数据插入将失败。在任何情况下,非键列都会复制索引表中的数据并增大索引的大小,从而可能会减慢搜索速度。请记住,在索引中包含有效负载列是没有意义的,除非表更改得足够慢以至于仅索引扫描可能不需要访问堆。如果无论如何都必须访问堆元组,则从那里获取列的值不会花费更多。其他限制是当前不支持将表达式作为包含列,并且目前只有 B-tree、GiST 和 SP-GiST 索引支持包含列。

在 PostgreSQL 之前包括功能,人们有时通过将有效负载列编写为普通索引列来覆盖索引,即编写

CREATE INDEX tab_x_y ON tab(x, y);

即使他们无意使用是的作为一个在哪里条款。只要额外的列是尾随列,这就可以正常工作;让他们成为领先的专栏是不明智的,原因在第 11.3 节.但是,此方法不支持您希望索引对键列强制唯一性的情况。

后缀截断总是从 B 树的上层移除非键列。作为有效负载列,它们从不用于指导索引扫描。当键列的剩余前缀恰好足以描述最低 B 树级别上的元组时,截断过程也会删除一个或多个尾随键列。在实践中,覆盖索引没有包括子句通常避免在上层存储有效载荷的列。但是,将有效负载列显式定义为非键列可靠保持上层的元组很小。

原则上,仅索引扫描可以与表达式索引一起使用。例如,给定一个索引f(x)在哪里x是表列,应该可以执行

SELECT f(x) FROM tab WHERE f(x) < 1;

作为仅索引扫描;这很有吸引力,如果F()是一个计算成本高的函数。但是,PostgreSQL 的规划器目前对这种情况不是很聪明。它认为只有在所有查询所需的可从索引中获得。在这个例子中,x除非在上下文中,否则不需要f(x),但规划器没有注意到这一点,并得出结论认为仅索引扫描是不可能的。如果仅索引扫描似乎足够值得,则可以通过添加x作为包含列,例如

CREATE INDEX tab_f_x ON tab (f(x)) INCLUDE (x);

一个额外的警告,如果目标是避免重新计算f(x), 是计划者不一定会匹配f(x)不可索引的在哪里索引列的子句。它通常会在如上所示的简单查询中做到这一点,但在涉及连接的查询中则不然。这些缺陷可能会在 PostgreSQL 的未来版本中得到弥补。

部分索引也与仅索引扫描有有趣的交互。考虑部分索引示例 11.3

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

原则上,我们可以对该索引进行仅索引扫描,以满足类似的查询

SELECT target FROM tests WHERE subject = 'some-subject' AND success;

但是有一个问题:在哪里子句是指成功不能作为索引的结果列使用。尽管如此,仅索引扫描是可能的,因为该计划不需要重新检查该部分的在哪里运行时子句:在索引中找到的所有条目都必须具有成功=真所以这不需要在计划中明确检查。PostgreSQL 版本 9.6 及更高版本将识别此类情况并允许生成仅索引扫描,但旧版本不会。