PostgreSQL 中的所有索引都是辅助索引,这意味着每个索引都单独存储在表的主数据区域(在 PostgreSQL 术语中称为表的堆)之外。这意味着在普通的索引扫描中,每次检索行都需要从索引和堆中获取数据。此外,虽然匹配给定可索引WHERE
条件的索引项通常在索引中是紧挨着的,但它们引用的表行可能位于堆中的任何位置。因此,索引扫描的堆访问部分涉及大量的随机堆访问,这可能很慢,尤其是在传统的旋转媒体上。(如11.5 节所述,位图扫描试图通过按排序顺序进行堆访问来减轻此成本,但这只能在一定程度上缓解。)
为了解决这个性能问题,PostgreSQL 支持仅索引扫描,它可以仅从索引回答查询,而无需任何堆访问。基本思想是从每个索引项直接返回值,而不是查阅关联的堆项。此方法的使用有两个基本限制:
索引类型必须支持仅索引扫描。B-tree 索引始终支持。GiST 和 SP-GiST 索引对某些运算符类支持仅索引扫描,但对其他运算符类不支持。其他索引类型不支持。基本要求是索引必须物理存储,或者能够重建每个索引项的原始数据值。作为反例,GIN 索引不支持仅索引扫描,因为每个索引项通常只包含原始数据值的一部分。
查询必须仅引用存储在索引中的列。例如,给定一个表上具有列z
的列x
和y
的索引,这些查询可以使用仅索引扫描:
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 要少得多。在大多数情况下,可见性映射会一直缓存在内存中。
总之,虽然在满足两个基本要求的情况下可以进行仅索引扫描,但只有当表中很大一部分堆页面的全部可见映射位都设置为 1 时,它才会有所收益。但是,数据行很大一部分不变的表很常见,这使得这种类型的扫描在实践中非常有用。
为了有效利用仅索引扫描功能,您可以选择创建一个覆盖索引,这是一种专门设计的索引,用于包含您频繁运行的特定类型查询所需的列。由于查询通常需要检索比搜索列更多的列,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
,而不适用于x
和y
的组合。(INCLUDE
子句也可以写在UNIQUE
和PRIMARY KEY
约束中,提供设置此类索引的替代语法。)
明智的做法是谨慎地向索引添加非键有效载荷列,尤其是宽列。如果索引元组超过了索引类型允许的最大大小,则数据插入将失败。无论如何,非键列会复制索引表中的数据,并会膨胀索引的大小,从而可能减慢搜索速度。请记住,除非表更改缓慢到仅索引扫描可能不需要访问堆,否则将有效载荷列包含在索引中几乎没有意义。如果必须访问堆元组,则从那里获取列的值不会带来额外成本。其他限制是表达式目前不支持作为包含的列,并且目前只有 B-tree、GiST 和 SP-GiST 索引支持包含的列。
在 PostgreSQL 拥有INCLUDE
功能之前,人们有时会通过将有效载荷列写成普通索引列来创建覆盖索引,即写成:
CREATE INDEX tab_x_y ON tab(x, y);
即使他们从未打算将y
用作WHERE
子句的一部分。只要额外的列是尾随列,这就可以正常工作;将它们作为前导列是不明智的,原因在11.3 节中解释过。然而,这种方法不支持您希望索引强制对键列执行唯一性的情况。
后缀截断始终会从上层 B-Tree 中删除非键列。作为有效载荷列,它们从不用于指导索引扫描。截断过程还会移除一个或多个尾随键列,当剩余的键列前缀足以描述最低层 B-Tree 上的元组时。实际上,没有INCLUDE
子句的覆盖索引通常会避免在上层存储有效载荷的列。然而,显式地将有效载荷列定义为非键列可靠地使上层中的元组保持较小。
原则上,仅索引扫描可以与表达式索引一起使用。例如,给定一个在x
(表列)上的表达式索引f(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
,而它在索引的结果列中不可用。尽管如此,仅索引扫描是可能的,因为计划在运行时不需要重新检查WHERE
子句的该部分:索引中的所有条目必然满足success = true
,因此无需在计划中明确检查。PostgreSQL 9.6 及更高版本将识别此类情况并允许生成仅索引扫描,但旧版本则不行。
如果您在文档中发现任何不正确的内容、与您对特定功能的体验不符的内容,或需要进一步说明的内容,请使用此表单报告文档问题。