仅索引扫描
仅索引扫描是 PostgreSQL 9.2 中添加的一项主要性能功能。它允许某些类型的查询仅通过从索引检索数据来满足,而不是从表中检索数据。这可以显著减少满足查询所需的 I/O 量。
在常规索引扫描期间,索引以类似于任何其他树结构的方式遍历,通过将常量与存储在索引中的数据项进行比较。Btree 索引的类型必须满足三等分属性;也就是说,该类型必须遵循自反、对称和传递定律。这些定律符合我们对类型应该如何表现的直观理解,但索引的物理结构反映了数据项的相对值这一事实实际上要求这些规则被类型遵循。Btree 索引包含技术上冗余的被索引的列数据的副本。
PostgreSQL 索引不包含可见性信息。也就是说,无法直接确定任何给定元组是否对当前事务可见,这就是为什么仅索引扫描的实现花了这么长时间的原因。编写一个具有廉价但可靠的可见性旁路查看的实现证明具有挑战性。
该功能的实现不成比例地涉及使现有称为可见性映射的磁盘上结构成为崩溃安全的。该结构必须可靠地(并且廉价地)指示索引元组的可见性 - 做得少一些意味着可能会导致仅索引扫描产生不正确的结果,这当然绝对不可接受。
索引只包含实际被索引的数据,而不是其他未索引的列这一事实,自然地排除了在查询其他列(例如出现在查询选择列表中)时使用仅索引扫描。
原则上可以使用仅索引扫描的示例查询
假设在某一列(通常是主键)上存在一些(非表达式)索引
select count(*) from categories;
假设在(第一个索引列,第二个索引列)上存在复合索引
select 1st_indexed_col, 2nd_indexed_col from categories;
PostgreSQL 9.2 添加了允许在普通索引扫描和仅索引扫描中使用 indexed_col op ANY(ARRAY[...]) 条件的功能。以前,此类条件只能在位图索引扫描中使用。因此,可以为这些 ScalarArrayOpExpr 查询看到仅索引扫描
select indexed_col from categories where indexed_col in (4, 5, 6);
仅索引扫描和索引访问方法
仅索引扫描实际上并不局限于在 btree 索引上的扫描。SP-GiST 运算符类可能支持仅索引扫描。
postgres=# select amname, amcanreturn from pg_am where amcanreturn != 0; amname | amcanreturn --------+-------------- btree | btcanreturn spgist | spgcanreturn (2 rows)
SP-GiST 运算符类可能暗示也可能不暗示实际的磁盘上索引是“有损”的;只有对于某些运算符类才会存储数据项的完整冗余副本,因此仅索引扫描实际上只被某些 SP-GiST 索引支持。对其他索引 AM 的支持可能会在 PostgreSQL 的未来版本中实现 - 类似于 btree_gist 和 btree_gin 的 GiST 和 GIN 运算符类,或者在 9.3 中,SP-GiST 的“范围上的四叉树”运算符类,不是有损的,因此原则上可以支持仅索引扫描。此外,即使使用有损索引,原则上仍然可以解决“select count(*)”查询,这可能会在未来的版本中实现。
可见性映射(和其他关系分支)
可见性映射是一个与每个堆关系(表)关联的简单数据结构。它是一个“关系分支”;与特定关系(表或索引)关联的磁盘上辅助文件。请注意,索引关系(即索引)没有与之关联的可见性映射。可见性映射关注的是在高级别上跟踪哪些元组对所有事务可见。来自一个事务的元组可能对任何给定的事务可见,也可能不可见,具体取决于它们的发起事务是否实际提交(尚未提交或从未提交,如果事务中止),以及何时相对于我们事务的当前快照发生。请注意,确切的行为取决于我们的事务隔离级别。还要注意,一个事务可能看到一个逻辑元组的一个物理元组/值集,而另一个事务可能看到相同逻辑元组的其他不同值,因为实际上,这两个事务对构成“现在”的理解不同。这是 MVCC 的核心思想。当所有事务都绝对一致地认为堆页面中的所有物理元组(行版本)都可见时,该页面的相应位可能被设置。
您可能熟悉的另一个关系分支是空闲空间映射。与可见性映射相反,堆关系和索引关系(唯一的例外是哈希索引关系,它没有空闲空间映射)都有 FSM。
空闲空间映射的目的是快速找到一个具有足够空闲空间来容纳要存储的元组的页面,或者确定是否存在这样的页面以及是否需要扩展关系。
在 PostgreSQL 8.4 中,添加了当前的空闲空间映射实现。它使空闲空间映射成为磁盘上关系分支。以前的实现要求管理员估计关系的数量,以及每个关系所需的空闲空间映射大小,以便空闲空间映射仅存在于共享内存的固定分配中。由于尺寸过小,这往往会导致浪费空间,因为核心系统的存储管理器不必要地扩展了关系。
[peter@peterlaptop 12935]$ ls -l -h -a -rw-------. 1 peter peter 8.0K Sep 28 00:00 12910 -rw-------. 1 peter peter 24K Sep 28 00:00 12910_fsm -rw-------. 1 peter peter 8.0K Sep 28 00:00 12910_vm ***SNIP***
FSM 被构建为二叉树 [1]。每个堆页面有一个叶节点,非叶节点存储其所有子节点的最大空闲空间量。因此,与 EXPLAIN 输出的节点成本不同,这些值不是累积的。
可见性映射是一个更简单的结构。可见性映射所对应的堆关系中的每个页面都有一个位。
拥有和维护可见性映射的主要实际原因是优化 VACUUM。设置的位表示已知对应堆页面上的所有元组对所有事务都可见,因此不需要对该页面进行真空操作。与新的空闲空间映射实现一样,可见性映射是在 Postgres 8.4 中添加的。
可见性映射是保守的,因为设置的位(1)表示该页面上的所有元组都可见,而未设置的位(0)表示该条件可能为真,也可能不为真 [2].
崩溃安全性、恢复和可见性映射
这涉及在 VACUUM 期间使用 WAL 记录设置可见性映射中的位,并在恢复期间采取各种特殊措施。
Postgres 预写日志被广泛用于确保崩溃安全性,但它也是内置热备用/流式复制功能的组成部分。
恢复将标记页面为全可见视为对快照的恢复冲突,这些快照可能仍然无法看到该页面上的 XID。PostgreSQL 未来可能会尝试缓和这种情况,以便该实现只强制索引扫描在可能出现问题的情况下执行堆提取,而不是抛出硬冲突。
覆盖索引
覆盖索引是专门为了用于仅索引扫描而创建的索引。它们通常“覆盖”比其他索引更合理的列,通常是已知是特定昂贵、频繁执行的查询的选择列表的一部分的列。PostgreSQL 支持在常规索引扫描中仅使用索引的前几列,如果这些列在查询的谓词中,因此覆盖索引对于常规索引扫描不一定是完全无用的。
与 HOT 的交互
HOT(仅堆元组)是 Postgres 8.3 中添加的一项主要性能功能。这允许对行的更新(由于 Postgres 的 MVCC 架构,这些更新是用物理元组的删除和插入来实现的)只需要在插入时创建新的物理堆元组,而不需要创建新的索引元组,当且仅当更新不影响索引列时。
使用 HOT,索引扫描可以遍历所谓的 HOT 链;它可以从物理索引元组(它可能由最初的 INSERT 创建,并与逻辑元组的早期版本相关)获取到相应的物理堆元组。堆元组本身将包含指向元组下一个版本的指针(即元组 ctid),该指针可能反过来又包含它自己的指针。索引扫描最终到达元组,该元组根据查询的快照是当前的。
HOT 还支持机会性小型真空操作,其中 HOT 链被“修剪”。
总而言之,这种性能优化被发现非常有价值,特别是对于 OLTP 工作负载。频繁更新的元组通常不被索引是相当自然的。但是,在考虑创建覆盖索引时,应仔细权衡最大化 HOT 更新数量的必要性。
可以使用此查询监控每个关系的 HOT 更新的总比例。
postgres=# select n_tup_upd, n_tup_hot_upd from pg_stat_user_tables;
哪些类型的查询可以通过仅索引扫描来满足?
除了查询不能引用未被单个索引索引的列以使用仅索引扫描的明显限制之外,访问所有元组的可见性都未知的堆的必要性相对昂贵。计划程序在考虑仅索引扫描时会非常重视这一因素,通常,需要确保表的绝大部分元组都设置了可见性映射位,可能会将仅索引扫描的有用性限制在针对不常更新的表的查询。
所有位都无需设置;如果需要,仅索引扫描可能会“访问堆”。实际上,仅索引扫描是一个有点用词不当的名称 - 索引大部分扫描可能是一个更合适的名称。涉及仅索引扫描的 explain analyze 将指示这种情况在实践中发生的频率。
postgres=# explain analyze select count(*) from categories; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------ Aggregate (cost=12.53..12.54 rows=1 width=0) (actual time=0.046..0.046 rows=1 loops=1) -> Index Only Scan using categories_pkey on categories (cost=0.00..12.49 rows=16 width=0) (actual time=0.018..0.038 rows=16 loops=1) Heap Fetches: 16 Total runtime: 0.108 ms (4 rows)
随着计划程序预计需要的堆提取(或“访问”)数量的增加,计划程序最终会得出结论,仅索引扫描不可取,因为它根据其成本模型不是最便宜的可能计划。仅索引扫描的价值完全在于它们有可能让我们省略堆访问(即使只是部分地)并最大程度地减少 I/O。
“count(*)” 现在快得多吗?
对 PostgreSQL 的一个传统抱怨(通常是在将它与 MySQL 不利地比较时,至少在使用 MyIsam 存储引擎时,该引擎不使用 MVCC)一直是“count(*) 很慢”。仅索引扫描 *可以* 用于满足这些查询,而无需有任何谓词来限制返回的行数,也无需强制使用索引来指定元组应该按索引列排序。但是,在实践中,这不太可能。
重要的是要意识到,规划器关注的是最小化查询的总成本。对于数据库来说,I/O 成本通常占主导地位。因此,"不带任何谓词的 count(*) "查询只有在索引明显小于其表时才会使用索引仅扫描。这通常只发生在表的行宽远大于某些索引的行宽时。
为什么我的查询没有使用索引仅扫描?
VACUUM 并没有特别倾向于更积极地运行,以更频繁地使用索引仅扫描。虽然 VACUUM 可以通过多种方式设置为更积极地运行,但目前尚不清楚为了更频繁地进行索引仅扫描而专门这样做是否明智。
规划器在考虑索引仅扫描时不会直接检查关系的整个可见性映射(然而,执行器会维护一个运行中的计数,这在 explain analyze 输出中可见)。但是,规划器确实会自然地权衡已知对所有人可见的页面的比例。
在 PostgreSQL 9.2 中,收集了有关已知对所有人可见的页面的比例的统计信息。pg_class.relallvisible 列指示有多少页面是可见的(可以通过将其计算为 pg_class.relpages 的比例来获得该比例)。这些统计信息会在运行 VACUUM 时更新。建议在升级到 PostgreSQL 9.2 后立即运行 VACUUM ANALYZE,以确保 relallvisible 大致符合实际情况。
请注意,可以通过检查 pg_stat_user_indexes.idx_scan 来检查索引扫描的数量(包括索引仅扫描和位图索引扫描)。如果您的覆盖索引没有被使用,您本质上是在为维护它的开销付费,而没有得到任何回报。删除该索引!
总结
索引仅扫描可以大大减少执行某些查询所需的 I/O 量。对于某些查询,特别是数据仓库特性的查询(即相对大量的静态、不常更新的数据,其中经常需要对历史数据的报告),它们可以显著提高性能。已经观察到,这些查询使用索引仅扫描的速度是之前的两倍到二十倍。但是,需要注意的是
- 索引仅扫描是机会主义的,它利用了预先存在的状态,在这种状态下可以避免堆访问。但是,服务器不会做出任何特别的努力来促进索引仅扫描,并且很难推荐采取措施来更频繁地进行索引仅扫描,除了根据测量的需求定义覆盖索引(例如,当 pg_stat_statements 指示执行针对相当静态数据的查询使用的 I/O 量过多时,并且只检索了一小部分表列)。
- 创建覆盖索引时,应该认真权衡对 HOT 更新的可能影响。该表上是否存在许多 HOT 更新?这是一个普遍的关注点,因为创建索引可能会阻止 HOT 更新的发生,并且因为 HOT 更新的数量是该表静态程度的合理指标(即插入、更新和删除的总数),因此也是大多数堆页面在任何给定时间都已知对所有人可见的可能性。
- 只有当规划器推测这样做会根据其不完美的基于成本的建模来减少所需的 I/O 总量时,才会使用索引仅扫描。这完全取决于元组的可见性,如果无论如何都会使用索引(即谓词的选择性如何等等),以及是否实际上存在一个原则上可以被索引仅扫描使用的索引。