11.11. 仅索引扫描

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 位的情况下,它才是成功的。但是,其中大部分行不变的表足够普遍,因此在实践中使这种类型的扫描非常有用。

为了有效利用仅索引扫描功能,您可以选择创建索引,在这些索引中,仅前导列与WHERE子句匹配,而尾随列保留要由查询返回的“有效载荷”数据。例如,如果您通常运行类似

SELECT y FROM tab WHERE x = 'key';

加快此类查询速度的传统方法是仅在x上创建索引。但是,在(x, y)上的索引将提供将此查询实现为仅索引扫描的可能性。如前所述,这样的索引比单独的x的索引更大,因此更昂贵,因此仅在已知该表大部分为静态的情况下才具有吸引力。请注意,在(x, y)而不是(y, x)上声明索引很重要,因为对于大多数索引类型(尤其是 B 树)而言,不限制前导索引列的搜索效率不是很高。

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

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

作为仅索引扫描;如果f()对于计算而言是昂贵的函数,则这非常有吸引力。但是,PostgreSQL 的计划者目前对这种情况不太聪明。仅当查询所需的所有都可从索引获得时,它才认为该查询可以通过仅索引扫描执行。在此示例中,除了上下文f(x)之外不需要x,但是计划人员没有注意到这一点,并得出结论认为,仅索引扫描是不可能的。如果仅索引扫描似乎足够值得,可以通过将索引声明为(f(x), 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 和更高版本将识别这种情况,并允许生成仅索引扫描,但较旧的版本则不会。