2024年9月26日: PostgreSQL 17 发布!
支持的版本:当前 (17) / 16 / 15 / 14 / 13 / 12
开发版本:devel
不受支持的版本:11 / 10 / 9.6

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

所有 PostgreSQL 中的索引都是辅助索引,这意味着每个索引都存储在表的主数据区域(在 PostgreSQL 术语中称为表的)之外。这意味着在普通的索引扫描中,每个行的检索都需要从索引和堆中获取数据。此外,虽然与给定可索引WHERE条件匹配的索引条目通常在索引中彼此靠近,但它们引用的表行可能位于堆中的任何位置。因此,索引扫描的堆访问部分涉及大量对堆的随机访问,这可能很慢,尤其是在传统的旋转介质上。(如第 11.5 节所述,位图扫描尝试通过以排序顺序进行堆访问来减轻此成本,但这只能做到这一点。)

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

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

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

简而言之,虽然在满足两个基本要求的情况下可以进行仅索引扫描,但只有当表堆页面的很大一部分都设置了其全可见映射位时,它才会成为优势。但是,其中很大一部分行保持不变的表在实践中很常见,因此这种类型的扫描非常有用。

为了有效地使用仅索引扫描功能,您可以选择创建一个覆盖索引,它是一个专门设计用于包含您频繁运行的特定类型查询所需的列的索引。由于查询通常需要检索的列不仅仅是它们搜索的列,因此 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 的组合。(INCLUDE 子句也可以写在 UNIQUEPRIMARY KEY 约束中,提供设置类似索引的替代语法。)

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

PostgreSQL 具有 INCLUDE 功能之前,人们有时会通过将有效负载列编写为普通索引列来创建覆盖索引,即编写

CREATE INDEX tab_x_y ON tab(x, y);

即使他们从未打算将 y 用作 WHERE 子句的一部分。只要额外的列是尾随列,这就可以正常工作;使它们成为前导列是不明智的,原因如第 11.3 节所述。但是,此方法不支持您希望索引强制执行键列唯一性的情况。

后缀截断始终从上层 B 树级别中删除非键列。作为有效负载列,它们永远不会用于指导索引扫描。当键列的其余前缀碰巧足以描述最低 B 树级别上的元组时,截断过程还会删除一个或多个尾随键列。在实践中,没有 INCLUDE 子句的覆盖索引通常会避免在上层存储实际上是有效负载的列。但是,将有效负载列显式定义为非键列可靠地使上层中的元组保持较小。

原则上,仅索引扫描可以与表达式索引一起使用。例如,给定一个 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),则计划程序不会一定将不在可索引 WHERE 子句中的 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;

但存在一个问题:WHERE 子句引用了 success,它不能作为索引的结果列提供。尽管如此,仅索引扫描仍然是可能的,因为计划不需要在运行时重新检查 WHERE 子句的这一部分:在索引中找到的所有条目都必然具有 success = true,因此不需要在计划中显式检查这一点。PostgreSQL 9.6 及更高版本将识别此类情况并允许生成仅索引扫描,但旧版本将不会。

提交更正

如果您在文档中发现任何错误、与您对特定功能的体验不符或需要进一步说明的内容,请使用此表单 报告文档问题。