支持的版本:当前 (17) / 16 / 15 / 14 / 13
开发版本:devel
不支持的版本:12 / 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 的规划器目前对此类情况不是很智能。仅当查询所需的所有都可以从索引中获得时,它才认为查询可以执行仅索引扫描。在此示例中,除了在 f(x) 上下文中之外,不需要 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,而 success 并非索引的结果列。尽管如此,仍然可以进行仅索引扫描,因为该计划不需要在运行时重新检查 WHERE 子句的那一部分:索引中找到的所有条目必然都有 success = true,因此无需在计划中显式检查。 PostgreSQL 9.6 及更高版本会识别这种情况并允许生成仅索引扫描,但较旧版本不会。

提交更正

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