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

PostgreSQL 中的所有索引都是次要索引,这意味着每个索引都与表的主数据区分开存储(在 PostgreSQL 术语中称为表的 heap *)。这意味着在普通索引扫描中,每行检索都需要从索引和堆中获取数据。此外,虽然与给定可索引WHERE条件匹配的索引条目通常在索引中靠在一起,但它们引用的表行可能在堆中的任何位置。因此,索引扫描的堆访问部分涉及对堆的大量随机访问,这可能很慢,尤其是在传统旋转媒体上。 (如Section 11.5中所述,位图扫描尝试通过按排序 Sequences 进行堆访问来减轻这种开销,但这仅适用于此。)

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

  • 索引类型必须支持仅索引扫描。 B 树索引始终有效。 GiST 和 SP-GiST 索引支持某些操作符类别的仅索引扫描,但不支持其他操作符类别。其他索引类型不支持。基本要求是索引必须物理存储或能够重建每个索引条目的原始数据值。作为反例,GIN 索引不支持仅索引扫描,因为每个索引条目通常仅保留原始数据值的一部分。

  • 该查询必须仅引用存储在索引中的列。例如,给定一个表的第xy列的索引,该表也有第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 快照“可见”每条检索到的行,如Chapter 13中所述。可见性信息不存储在索引条目中,而仅存储在堆条目中。因此乍一看似乎每行检索都将需要堆访问。如果表行最近被修改,的确是这种情况。但是,对于很少更改数据,有一种方法可以解决此问题。 PostgreSQL 针对表堆中的每个页面跟踪该页面中存储的所有行是否足够旧,以至于所有当前和将来的事务均可见。此信息存储在表格的可见性 Map中。仅索引扫描在找到候选索引条目之后,检查相应堆页面的可见性 Map 位。如果已设置,则该行是可见的,因此无需进一步的工作即可返回数据。如果未设置,则必须访问堆条目以了解它是否可见,因此与标准索引扫描相比,不会获得性能优势。即使在成功的情况下,这种方法也将可见性 Map 访问权换成堆访问权。但是由于可见性图比其描述的堆小四个数量级,因此访问它所需的物理 I/O 少得多。在大多数情况下,可见性图始终保持缓存在内存中。

简而言之,尽管在满足两个基本要求的情况下可以进行仅索引扫描,但是只有在表的堆页面的很大一部分都设置了其所有可见 Map 位的情况下,它才是成功的。但是,其中大部分行不变的表足够普遍,因此在实践中使这种类型的扫描非常有用。

为了有效利用仅索引扫描功能,您可以选择创建一个* covering index *,这是一个专门设计为包含经常运行的特定类型查询所需要的列的索引。由于查询通常需要检索的列不仅仅是查询的列,因此 PostgreSQL 允许您创建一个索引,其中某些列只是“有效载荷”,而不是搜索键的一部分。这是通过添加列出多余列的INCLUDE子句来完成的。例如,如果您通常运行类似

SELECT y FROM tab WHERE x = 'key';

加快此类查询速度的传统方法是仅在x上创建索引。但是,索引定义为

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

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

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

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

唯一性条件仅适用于x列,不适用于xy的组合。 (也可以用UNIQUEPRIMARY KEY约束编写INCLUDE子句,从而提供用于设置这样的索引的替代语法.)

在向索引添加非关键有效负载列(尤其是宽列)时要保守一些是明智的。如果索引 Tuples 超出了索引类型允许的最大大小,则数据插入将失败。在任何情况下,非键列都会复制索引表中的数据,并且会使索引的大小膨胀,从而有可能减慢搜索速度。并且请记住,除非表变化足够慢以至于仅索引扫描可能不需要访问堆,否则将有效负载列包含在索引中没有什么意义。如果无论如何都必须访问堆 Tuples,那么从那里获取列的值就不需要花费更多。其他限制是当前不支持将表达式作为包含列,并且仅 B 树索引当前支持包含列。

在 PostgreSQL 具有INCLUDE功能之前,人们有时通过将有效载荷列写为普通索引列来覆盖索引,即

CREATE INDEX tab_x_y ON tab(x, y);

即使他们无意将y用作WHERE子句的一部分。只要多余的列是尾随列,这就可以正常工作;由于Section 11.3中说明的原因,使其成为领先者是不明智的。但是,此方法不支持您希望索引在键列上强制唯一性的情况。同样,将不可搜索的列显式标记为INCLUDE列会使索引稍小,因为此类列不必存储在较高的 B 树级别中。

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

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

作为仅索引扫描;如果f()对于计算而言是昂贵的函数,则这非常有吸引力。但是,PostgreSQL 的计划者目前对这种情况不太聪明。仅当查询所需的所有都可从索引获得时,它才认为该查询可以通过仅索引扫描执行。在此示例中,除了上下文f(x)之外不需要x,但是计划者没有注意到这一点,并得出结论认为不可能进行仅索引扫描。如果仅索引扫描似乎足够值得,可以通过添加x作为包含列来解决此问题,例如

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

如果要避免重新计算f(x),则另一个警告是计划者不必将不在可索引WHERE子句中的f(x)的用法与索引列匹配。通常,在如上所示的简单查询中,这样做会正确,但在涉及联接的查询中则不会。这些缺陷可以在 PostgreSQL 的 Future 版本中进行纠正。

部分索引还与仅索引扫描有有趣的交互。考虑Example 11.3中显示的部分索引:

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

原则上,我们可以对此索引执行仅索引扫描,以满足诸如

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

但是有一个问题:WHERE子句指向success,它不能作为索引的结果列使用。但是,仅执行索引扫描是可能的,因为该计划不需要在运行时重新检查WHERE子句的该部分:索引中找到的所有条目都必须具有success = true,因此无需在计划中对其进行明确检查。 PostgreSQL 9.6 和更高版本将识别这种情况,并允许生成仅索引扫描,但较旧的版本则不会。