PostgreSQL 的堆内元组和仅索引扫描

本文是《PostgreSQL指南--内幕探索》(铃木启修著 冯若航 刘阳明 张文升译)的读书笔记,仅供自己学习使用,请勿转载。这是一本好书,如有需要请直接购买书籍。
本章将介绍两个和索引扫描有关的特性,分别是堆内元组(heap only tuple,HOT)和仅索引扫描。

1. 堆内元组

在 8.3 版本中实现的HOT特性,使得更新行的时候,可以将新行放置在旧行所处的同一个数据页中,从而高效地利用索引与表的数据页。HOT特性减少了不必要的清理过程。

在源码的README.HOT中有关于HOT的详细介绍,本章只是简单地介绍HOT。首先,第 1.1 节描述了在没有HOT特性的时候,更新一行是一个怎样的过程,以阐明要解决的问题。接下来,在第 1.2 节中将介绍HOT做了什么。

1.1 没有HOT时的行更新

假设表tbl有两个列:id和data,id是tbl的主键。

    testdb=# \d tbl
                    Table "public.tbl"
     Column |  Type   | Collation | Nullable | Default
    --------+---------+-----------+----------+---------
     id      | integer |             | not null |
     data   | text     |             |            |
    Indexes:
        "tbl_pkey" PRIMARY KEY, btree (id)

表tbl有1000条元组,最后一个元组的id是1000,存储在第5个数据页中。最后一条元组被相应的索引元组所引用,索引元组的key是1000,且tid是(5,1)。如图 1(1) 所示:

没有HOT的行更新.png

​ 图 1 没有使用 HOT 技术的行更新

我们考虑一下,没有HOT特性时,最后一个元组是如何更新的。

    testdb=# UPDATE tbl SET data = 'B' WHERE id = 1000;

在该场景中,PostgreSQL不仅要插入一条新的表元组,还需要在索引页中插入新的索引元组,如图 1(2)所示。索引元组的插入消耗了索引页的空间,而且索引元组的插入和清理都是开销很大的操作。HOT的目的,就是降低这种影响。

1.2 HOT 如何工作

当使用 HOT 特性更新行时,如果被更新的元组存储在老元组所在的页面中,PostgreSQL就不会再插入相应的索引元组,而是分别设置新元组的HEAP_ONLY_TUPLE标记位与老元组的HEAP_HOT_UPDATED 标记位,两个标记位都保存在元组的 t_informask2 字段中,如图 2 和图 3 所示。

比如在这个例子中, Tuple_1 和 Tuple_2 分别被设置成 HEAP_HOT_UPDATEDHEAP_ONLY_TUPLE

另外,在修剪和碎片整理处理过程中,都会使用下面介绍的HEAP_HOT_UPDATEDHEAP_ONLY_TUPLE标记位。

使用HOT的行更新.png

​ 图 2 使用 HOT 技术的行更新

HEAP_HOT_UPDATED和HEAP_ONLY_TUPLE标记位.png

​ 图 3 HEAP_HOT_UPDATED和HEAP_ONLY_TUPLE标记位

接下来会介绍,当基于HOT更新一个元组后,PostgreSQL是如何在索引扫描中访问这些被 HOT 更新的元组的,如图 4 (1)所示:

行指针修剪.png

​ 图 4 行指针修剪(行指针重定向)前后的索引扫描过程

(1)找到指向目标数据元组的索引元组。

(2)按所获索引元组指向的位置访问行指针数组,找到行指针 1。

(3)读取 Tuple_1。

(4)经由 Tuple_1 的 t_ctid 字段,读取 Tuple_2。

在这种情况下,PostgreSQL会读取两条元组,分别是 Tuple_1 和 Tuple_2,并通过 PostgreSQL 的并发控制机制 来判断哪条元组是可见的。但如果数据页中的死元组已经被清理,就有问题了。比如在图 4(1)中,如果 Tuple_1 由于是死元组而被清理,就无法通过索引访问 Tuple_2 了。

为了解决这个问题,PostgreSQL 会在合适的时候进行 行指针重定向,将指向老元组的行指针重新指向新元组的行指针。在 PostgreSQL 中,这个过程称为 修剪。图 4(2)说明了 PostgreSQL 在修剪之后如何访问更新的元组。

  1. 找到索引元组。

  2. 通过索引元组,找到行指针1。

  3. 通过重定向的行指针1,找到行指针2。

  4. 通过行指针2,读取Tuple_2。

可能的话,剪枝任何时候都有可能会发生,比如 SELECT 、UPDATE、 INSERT 、DELETE 这类SQL命令执行的时候,确切的执行时机不会在本章中描述,因为它太复杂了。具体细节可以在 README.HOT 文件中找到。

在 PostgreSQL 执行剪枝时,如果可能,会挑选合适的时机来清理死元组。在 PostgreSQL 中,这种操作称为碎片整理,图 5 描述了 HOT 中死元组的碎片整理过程。

HOT技术中的死元组的碎片整理.png

​ 图 5 HOT 技术下的死元组的碎片整理

需要注意的是,因为碎片整理的工作并不涉及索引元组的移除,所以碎片整理比起常规的清理开销要少得多。

因此,HOT特性降低了索引和表的空间消耗,同样减少了清理过程需要处理的元组数量。由于减少了更新操作需要插入的索引元组数量,并减少了清理操作需要处理的元组数量,HOT对于性能提高有良好的促进作用。

2 仅索引扫描

当 SELECT 语句的所有目标列都在索引键中时,为了减少 I/O 代价,仅索引扫描(又叫仅索引访问)会直接使用索引中的键值。所有商业关系型数据库中都提供这个技术,比如 DB2 和 Oracle。PostgreSQL 在 9.2 版本中引入这个特性。

接下来我们会基于一个特殊的例子,介绍PostgreSQL中仅索引扫描的工作过程。首先是关于这个例子的假设。

  • 表定义

我们有一个tbl表,其定义如下所示:

    testdb=# \d tbl
          Table "public.tbl"
     Column |  Type   | Modifiers
    --------+---------+-----------
     id      | integer |
     name   | text     |
     data   | text     |
    Indexes:
        "tbl_idx" btree (id, name)
  • 索引

表tbl有一个索引tbl_idx,包含id和name两列。

  • 元组

tbl 已经插入了一些元组。

id=18, name = 'Queen' 的 Tuple_18 存储在 0 号数据页中。

id=19, name = 'BOSTON' 的 Tuple_19 存储在 1 号数据页中。

  • 元组可见性

假设:所有在 0号页面中的元组永远可见,1号页面中的元组并不总是可见的。注意,每个页的可见性信息都存储在相应的可见性映射中。

我们来研究一下,当下面的SELECT语句执行时,PostgreSQL是如何读取元组的。

    testdb=# SELECT id, name FROM tbl WHERE id BETWEEN 18 and 19;
     id |  name
    ----+--------
     18 | Queen
     19 | Boston
    (2 rows)
仅索引扫描.png

​ 图 6 仅索引扫描

查询需要从表中读取 id 和 name 两列,索引 tbl_idx 包含了这些列。因此在使用索引扫描时,第一眼看上去好像访问表的页面是没有必要的,因为索引中已经包含了必要的数据。然而原则上,PostgreSQL 有必要检查这些元组的可见性,索引元组中并没有任何关于堆元组的事务相关信息,比如 t_xmin 和 t_xmax。因此,PostgreSQL需要访问表数据来检查索引元组中数据的可见性,这就有点本末倒置的感觉了。

面对这种困境,PostgreSQL 使用目标数据表对应的可见性映射表来解决此问题。如果某一页中存储所有的元组都是可见的,PostgreSQL 就会使用索引元组,而不去访问索引元组指向的数据页去检查可见性,否则,PostgreSQL 读取索引元组指向的数据元组并检查元组可见性,而这就跟原来设想的一样。

​ 在这个例子中,因为的0号页面被标记为可见,因此0号页面中存储的包括Tuple_18在内的所有元组都是可见的,所以就不用再去访问Tuple_18了。相应地,因为1号页面并没有被标记为可见,此时为了检查并发控制的可见性,需要访问Tuple_19。如图6 所示,即为仅索引扫描的工作过程。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。