最近搞了一个公众号PostgreSQL运维技术,欢迎来踩~
悄悄放一张:
PostgreSQL运维技术
本章描述了与索引扫描相关的两个特性,即HOT 和 Index-Only Scans。
注:是The Internals of PostgreSQL第七章
7.1. Heap Only Tuple (HOT)
PG在8.3版本中实现了HOT, 主要是为了当要更新的行存储在与旧行相同的表页中时,有效地使用索引和表中的页面。Hot也减少了VACUUM处理的必要性。
7.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页,最后一个元组指向对应的索引元组,其键为'1000',tid为'(5,1)'。参见图7.1(a)。
图. 7.1.使用非HOT的方式更新行
我们考虑如何在不使用HOT的情况下更新最后一个元组。
testdb=# UPDATE tbl SET data = 'B' WHERE id = 1000;
在这种情况下,PostgreSQL不仅会在索引页中插入新的表元组,还会插入新的索引元组。见图7.1(b)。
索引元组的插入会消耗索引页空间,而且索引元组的插入和清除成本都很高。HOT降低了这些问题的影响。
7.1.2. HOT 是如何工作的?
当某一行数据使用HOT的方式被更新时,如果被更新的行与旧行数据存储在同一个页面,那么PG不会插入相应的索引元组。并且将老数据行的t_informask2字段设置为HEAP_HOT_UPDATE位, 新数据行的t_informask2字段设置为HEAP_ONLY_TUPLE位。参见图7.2和图7.3。
图. 7.2. 使用HOT的方式更新行
例如,在本例中,'Tuple_1'和'Tuple_2'分别被设置为HEAP_HOT_UPDATED位和HEAP_ONLY_TUPLE位。
图. 7.3. HEAP_HOT_UPDATED and HEAP_ONLY_TUPLE 位
接下来将会介绍PostgreSQL如何在使用HOT更新元组之后使用索引扫描来访问更新后的元组。
图. 7.4. 修剪行指针
(1)找到指向目标元组的索引元组。
(2)访问正在获取的索引元组所指向的行指针[1]。
(3)读“Tuple_1”。
(4)通过'Tuple_1'的t_ctid读取' Tuple_2 '
在这种情况下,PostgreSQL读取两个元组,'Tuple_1'和'Tuple_2',并使用第五章中描述的并发控制机制来决定哪个是可见的。
但是,如果表页中的死亡元组被删除,就会出现一个问题。例如,在图7.4(a)中,如果'Tuple_1'被删除,因为它是一个死元组,'Tuple_2'不能从索引访问。
为了解决这个问题,PostgreSQL在适当的时候将指向旧元组的行指针重定向到指向新元组的行指针。在PostgreSQL中,这种处理称为剪枝。图7.4(b)描述了PostgreSQL如何访问修剪后更新的元组。
(1)查找索引元组。
(2)访问正在获取的索引元组所指向的行指针[1]。
(3)通过重定向(redirect)的行指针访问指向Tuple_2的行指针[2]。
(4)读取从行指针'[2]'指向的' Tuple_2 '。
如果可能的话,将在执行SELECT、UPDATE、INSERT和DELETE等SQL命令时执行修剪处理。具体的执行时间在本章中没有描述,因为它非常复杂。详细信息在PG源码中HOT的自述文件中有介绍。见:https://github.com/postgres/postgres/blob/master/src/backend/access/heap/README.HOT
PostgreSQL会在适当的时候删除死元组,在PostgreSQL的文档中,这种处理称为碎片整理。图7.5描述了使用HOT进行碎片整理的过程。
图. 7.5. 对死亡元祖进行碎片整理
注意,碎片整理的成本低于正常VACUUM处理的成本,因为碎片整理不涉及删除索引元组。
因此,使用HOT可以减少索引和页表的消耗;这也减少了VACUUM处理必须处理的元组的数量。因此,HOT对性能有很好的影响,
另外,我们需要注意下HOT不可用的情况。
当更新后的元组存储在另一个页(该页不存储旧元组)时,指向该元组的索引元组也会插入到索引页中。参见图7.6(a)。
当索引元组的键值被更新时,新的索引元组被插入到索引页中。见图7.6(b)。
图. 7.6. Hot不可用的情况
7.2. Index-Only Scans
为了减少I/O(输入/输出)成本,当SELECT语句的所有目标条目都包含在索引键中时,仅索引扫描(通常称为仅索引访问)直接使用索引键而不访问相应的表页。几乎所有的商业RDBMS都提供了这种技术,比如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'已经插入元组。
'Tuple_18',它的id是'18',名称是' Queen ',存储在第0页。
'Tuple_19',它的id是'19',名称是'BOSTON',存储在第一页。
可见性
第0页中的所有元组总是可见的;第一页中的元组并不总是可见的。注意,每个页面的可见性存储在相应的可见性映射中,可见性映射在6.2节中描述。
让我们看看PostgreSQL在执行下面的SELECT命令时是如何读取元组的。
testdb=# SELECT id, name FROM tbl WHERE id BETWEEN 18 and 19;
id | name
----+--------
18 | Queen
19 | Boston
(2 rows)
该查询从表的两个列获取数据:'id'和'name',索引'tbl_idx'由这些列组成。因此,在使用索引扫描时,乍一看似乎不需要访问表页,因为索引元组包含必要的数据。然而,事实上,PostgreSQL原则上必须检查元组的可见性,索引元组没有任何关于事务的信息,比如堆元组的t_xmin和t_xmax,这在5.2节中有描述。因此,PostgreSQL必须访问表数据来检查索引元组中数据的可见性。
为了避免这种困境,PostgreSQL使用目标表的可见性映射。如果存储在一个页面中的所有元组都是可见的,PostgreSQL使用索引元组的键,而不访问索引元组指向的表页来检查其可见性;否则,PostgreSQL从index元组中读取指向的表元组,并检查元组的可见性。
在本例中,不需要访问' Tuple_18 ',因为存储' Tuple_18 '的第0页是可见的,也就是说,包括第0页中的Tuple_18在内的所有元组都是可见的。相反,需要访问' Tuple_19 '来处理并发控制,因为第一个页面的可见性不可见。参见图7.7。
图. 7.7. Index-Only Scans 是如何工作的?