PostgreSQL Vacuum—索引删除
预备知识
概述
在《PostgreSQL Vacuum—元组删除》中,我们现实阐述了元组的删除过程,从中我们知道,在删除索引之前HOT链的链头元组的ItemData只能被标记为LP_DEAD来防止重用,只有当索引删除之后ItemData才能标记为LP_UNUSED以供重用。所以本章我们将来阐述如何删除索引。索引的删除有如下两个场景:
-
用户执行Vaccum命令
-
索引执行插入操作
向索引插入index tuple时,如果发现待插入的页面没有足够的空闲空间,会先尝试对页面进行空间整理。
本文将重点阐述这两个场景的实现细节。
Lazy Vacuum
在《PostgreSQL Vacuum—元组删除》中我们阐述了Lazy Vaccum如何删除元组,现在我们可以来看看Lazy Vacuum如何删除索引。Vacuum操作要做的事情很多,包括删除元组、冻结事务标识、更新空间映射表、更新可见性映射表等,这里我们只关注删除元组操作。Lazy Vacuum的核心函数是lazy_scan_heap,这个函数非常长,就不看代码了,只讨论与删除相关的流程,在流程的每个步骤中给出代码位置:
-
步骤1:遍历表的所有数据块(
vacuumlazy.c line:585) -
步骤2:对每一个块调用heap_page_prune清理过期元组(
vacuumlazy.c line:911)通过《PostgreSQL Vacuum—元组删除》我们知道,heap_page_prune完成后,块中会留下标记为LP_DEAD的元组,我们将它们称为dead tuple。
-
步骤3:创建dead tuple数组记录表中所有元组dead tuple的tid(
vacuumlazy.c line:961) -
步骤4:删除dead tuple数组对应的索引元组(
lazy_vacuum_index()函数,vacuumlazy.c line:1304) -
步骤5:将dead tuple数组中所有元组的标识改为LP_UNUSED(
lazy_vacuum_heap()函数,vacuumlazy.c line:1317)
在这5个步骤中,我们需要重点关注步骤4,通过调用lazy_vacuum_index来删除索引,下面我们来看看lazy_vacuum_index的实现。
lazy_vacuum_index
lazy_vacuum_index最终会调用到btvacuumscan函数,btvacuumscan的核心代码如下:
blkno = BTREE_METAPAGE + 1;
for (;;)
{
/* Get the current relation length */
if (needLock)
LockRelationForExtension(rel, ExclusiveLock);
num_pages = RelationGetNumberOfBlocks(rel);
if (needLock)
UnlockRelationForExtension(rel, ExclusiveLock);
/* Quit if we've scanned the whole relation */
if (blkno >= num_pages)
break;
/* Iterate over pages, then loop back to recheck length */
for (; blkno < num_pages; blkno++)
{
btvacuumpage(&vstate, blkno, blkno);
}
}
btvacuumpage
上述代码遍历索引的所有块,对于每个块调用btvacuumpage对块的索引元组进行删除,该函数主要有三个步骤:
-
步骤1:遍历页面内索引元组,判断元组是否可以删除。
判断元组删除是通过调用callback函数来实现,callback是一个函数指针,实际指向lazy_tid_reaped函数,而lazy_tid_reaped会校验index tuple的tid是否存在于dead tuple数组中(此外dead tuple数组是按照tid升序排列,所以在校验的时候会采用二分法)。
-
步骤2:使用deletable数组记录需要删除的元组。
-
步骤3:调用_bt_delitems_vacuum删除deletable中的元组。
btvacuumpage的实现如下:
static void
btvacuumpage(BTVacState *vstate, BlockNumber blkno, BlockNumber orig_blkno)
{
IndexVacuumInfo *info = vstate->info;
IndexBulkDeleteResult *stats = vstate->stats;
IndexBulkDeleteCallback callback = vstate->callback;
void *callback_state = vstate->callback_state;
Relation rel = info->index;
bool delete_now;
BlockNumber recurse_to;
Buffer buf;
Page page;
BTPageOpaque opaque = NULL;
restart:
delete_now = false;
recurse_to = P_NONE;
/* call vacuum_delay_point while not holding any buffer lock */
vacuum_delay_point();
/*
* We can't use _bt_getbuf() here because it always applies
* _bt_checkpage(), which will barf on an all-zero page. We want to
* recycle all-zero pages, not fail. Also, we want to use a nondefault
* buffer access strategy.
*/
buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL,
info->strategy);
LockBuffer(buf, BT_READ);
page = BufferGetPage(buf);
if (!PageIsNew(page))
{
_bt_checkpage(rel, buf);
opaque = (BTPageOpaque) PageGetSpecialPointer(page);
}
/*
* If we are recursing, the only case we want to do anything with is a
* live leaf page having the current vacuum cycle ID. Any other state
* implies we already saw the page (eg, deleted it as being empty).
*/
if (blkno != orig_blkno)
{
if (_bt_page_recyclable(page) ||
P_IGNORE(opaque) ||
!P_ISLEAF(opaque) ||
opaque->btpo_cycleid != vstate->cycleid)
{
_bt_relbuf(rel, buf);
return;
}
}
/* Page is valid, see what to do with it */
if (_bt_page_recyclable(page))
{
/* Okay to recycle this page */
RecordFreeIndexPage

本文详细解析了PostgreSQL中索引的删除过程,包括在Vaccum操作和索引插入时如何处理。在Vaccum期间,通过LazyVacuum遍历表的数据块,调用heap_page_prune删除元组,然后使用lazy_vacuum_index删除索引元组。在索引插入时,若页面空间不足,会尝试通过btvacuumscan和_bt_vacuum_one_page进行空间整理,标记并删除LP_DEAD的索引元组。整个过程中,通过遍历HOT链判断元组是否过期,从而标记为LP_DEAD,最后统一进行删除操作。
最低0.47元/天 解锁文章

被折叠的 条评论
为什么被折叠?



