一、问题描述
数据库已运行很长时间,某天客户运维人员突然查询某个普通表上的某个索引的属性indcheckxmin,发现该属性是true,但索引仍可以正常使用。咨询为什么该索引的indcheckxmin=true,而表上其他索引的indcheckxmin=false。
二、初步分析过程
2.1 查询pg官网手册
indcheckxmin
bool
If true, queries must not use the index until the
xmin
of thispg_index
row is below theirTransactionXmin
event horizon, because the table may contain broken HOT chains with incompatible rows that they can see
雪友们,见谅哈!毕竟不是英专生,只能通俗的翻译一下就是:如果indcheckxmin = true,则查询业务 在 "这个索引元组的xmin" 小于 "事务的阈值" 之前是不能使用该索引,因为这个表可能包含一个 "断热链",即我们可以看见一个不兼容的行数据。
如果对数据库内核的研究功力不够深厚,可能无法一下子理解,不过接着往下看,必有 "复行数十步,豁然开朗" 的桃花源记feeling。
2.2 先了解一下什么叫"断链"
HOT链发生"断链" 的情况可能是两种:(1) 索引元组的键值更新 (2)当更新的元组存储在另一页中,和旧元组不存储在同一页。
见博客《openGauss内核求索 ---- HOT链》
2.3 代码分析
@第一步@:搜索到indcheckxmin = true的地方,先锁定到index_build函数中,分析if条件语句,首先ii_BrokenHotChain 不确定,!reindex = true,不是ustore表,不是分区表,不是并发创建索引场景(非CIC场景)。因此推测应该是 ii_BrokenHotChain = true 导致。
@第二步@:搜索ii_BrokenHotChain = true 的位置,找到IndexBuildHeapScan函数,发现在索引创建的时候,扫描heap表的时候,发现tuple是HEAPTUPLE_RECENTLY_DEAD情况。
@第三步@:在上一步基础上往上搜索代码,发现 result = HEAPTUPLE_RECENTLY_DEAD 来源于HeapTupleSatisfiesVaccum的结果。
@第四步@:查看HeapTupleSatisfiesVaccum函数,可知如果该tuple的修改已经提交,但xmax > OldestXmin,但返回HEAPTUPLE_RECENTLY_DEAD。
@第五步@:通过回溯上面四步可知场景如下,用户在表t1上创建索引,在创建索引过程中需要扫描数据表,每一条扫描到的tuple都需要做HeapTupleSatisfiesVaccum判断,其中一条tuple的xmax已提交但小于OldestXmin,因此认为该tuple是HEAPTUPLE_RECENTLY_DEAD情况(第四步),即表明该tuple最近做过修改, 可能是update或者是delete,然后在对该HEAPTUPLE_RECENTLY_DEAD情况具体分析,发现该tuple的标志位是 HEAP_HOT_UPDATED ,所以这条tuple可能不久前刚被update过,设置ii_BrokenHotChain = true (第二步),到这里就很疑惑为什么是设置这个参数?是2.2章节里面提到的"hot断链"的场景吗?接下来有点绕脑子了。
三、深入理论分析
3.1 场景1- 先create index后update索引键
如上图所示,(1)先创建表t1;(2) 创建索引,索引的叶子节点指向行指针tp1,行指针tp1指向tuple1;(3) update更新索引键值,如果被更新的行tuple2与旧行数据tuple1存储在同一个页面,则不会插入相应的索引元组,并将老数据行的t_informask2字段设置为HEAP_HOT_UPDATE位, 新数据行的t_informask2字段设置为HEAP_ONLY_TUPLE位。那么就形成了图中的一条关系链1:索引叶子节点索引元组 --> 行指针tp1 --> tuple1 --> tuple2,这就是使用HOT更新元组tuple1之后,使用索引扫描来访问更新后的元组的逻辑线,这个逻辑关系链先称之为"HOT链",是理想中的样子。
3.2 场景2 - 先update索引键后create index
如上图所示,(1)先创建表t1;(2) update更新索引键值,当前还没有创建索引,因此只涉及到数据页面,将老数据行的t_informask2字段设置为HEAP_HOT_UPDATE位, 新数据行的t_informask2字段设置为HEAP_ONLY_TUPLE位;(3)创建索引,根据章节二的代码分析可知,扫描tuple发现HEAP_HOT_UPDATE位,则该行不创建索引,即代码中indexIt=false。 而只更新hot链末位的tuple,如图中tuple2,因此逆向找到行指针tp2,索引创建叶子节点中的索引元组指向tp2,即关系链2为: 索引叶子节点索引元组 --> 行指针tp2 --> tuple2。@结论1@:所以当前的关系链2相较于场景1的关系链1,在tuple1还没有清理的情况下,可知tuple1-->tuple2这一关系断了,因此pg在这里称之为"BrokenHotChain ",是发生场景2后现实的样子。(理想是丰满的,现实是骨感的!)
3.3 进一步猜想
(还需要深入想一下,目前只是假设了一些情况,但是原因还不很充足)
根据场景2,可知发生了章节2.1-查询pg官网手册中描述的"table may contain broken HOT chains";并且在场景2中,可能存在 一些事务(xid=8) 是在 第三步 update(xmin=9) 之前发生,假设TransactionXmin=7,因此tuple2应该对 这些事务(xid<9) 不可见的,而在场景2情况下,索引是直接创建在指向最新tuple2的tp2上,如果不做任何检查机制,索引扫描会直接根据tp2找到tuple2,即pg官网手册中描述的" broken HOT chains with incompatible rows that they can see" ,这些事务可能会看到不兼容的行(因为索引元组没有可见性判断,刚好修改的是索引键,为了减少I/O(输入/输出)成本,当SELECT语句的所有目标条目都包含在索引键中时,仅索引扫描(通常称为仅索引访问)直接使用索引键而不需要回表访问相应的表页。几乎所有的商业RDBMS都提供了这种技术,比如DB2和Oracle,PostgreSQL从9.2版开始引入了这个选项),因此根据可见性表的判断可能会直接返回当前索引元组的值,所以需要一些其他机制来检查可见性。另外一种是 第三步 update(xmin=6) ,假设TransactionXmin=7,那么查询事务此时都不应该使用该索引,没有机制可以检查不兼容的行的可见性。是因此设置indcheckxmin=true,来表明是场景2情况。
3.4 从代码分析indcheckxmin的使用
在仅索引扫描情况下,根据indcheckxmin=true,如果当前索引表元组的xmin > TransactionXmin,则表明发生了场景2,创建索引不久前刚做过update,所以需要通过CSN来检查可见性。即官网手册中描述的"queries must not use the index until the xmin
of this pg_index
row is below their TransactionXmin
event horizon"。
@结论@
如果indcheckxmin = true
当这个索引元组的xmin < 事务的阈值时,查询业务不能使用索引,索引不可用
当这个索引元组的xmin > 事务的阈值时,查询业务能使用索引,但需要通过CSN检查可见性,索引可用
代码如下:
/*
* If the index is valid, but cannot yet be used, ignore it; but
* mark the plan we are generating as transient. See
* src/backend/access/heap/README.HOT for discussion.
*/
if (index->indcheckxmin) {
TransactionId xmin = HeapTupleGetRawXmin(indexRelation->rd_indextuple);
if (RelationIsUBTree(indexRelation) && TransactionIdIsCurrentTransactionId(xmin) &&
!IsolationUsesXactSnapshot()) {
/* new created ubtree index of the current Read Committed transaction is still valid */
} else if (!TransactionIdPrecedes(xmin, u_sess->utils_cxt.TransactionXmin)) {
/*
* Since the TransactionXmin won't advance immediately(see CalculateLocalLatestSnapshot),
* we need to check CSN for the visibility.
*/
CommitSeqNo csn = TransactionIdGetCommitSeqNo(xmin, false, true, false, NULL);
if (!COMMITSEQNO_IS_COMMITTED(csn) || csn >= u_sess->utils_cxt.CurrentSnapshot->snapshotcsn) {
root->glob->transientPlan = true;
index_close(indexRelation, NoLock);
continue;
}
}
复制