From cb3349f5a838099b386ad490a664801f96473678 Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 8 Mar 2019 21:24:37 +0500 Subject: [PATCH 2/6] Delete pages during GiST VACUUM v22 This commit teaches GiST to actually delete pages during VACUUM. To do this we scan GiST two times. At first pass we make notes of empty pages and internal pages. At second pass we scan through internal pages looking for references to empty leaf pages. --- src/backend/access/gist/README | 14 ++ src/backend/access/gist/gist.c | 18 +++ src/backend/access/gist/gistutil.c | 3 +- src/backend/access/gist/gistvacuum.c | 186 ++++++++++++++++++++++++- src/backend/access/gist/gistxlog.c | 60 ++++++++ src/backend/access/rmgrdesc/gistdesc.c | 3 + src/include/access/gist.h | 4 + src/include/access/gist_private.h | 7 +- src/include/access/gistxlog.h | 10 +- src/test/regress/expected/gist.out | 6 +- src/test/regress/sql/gist.sql | 6 +- 11 files changed, 305 insertions(+), 12 deletions(-) diff --git a/src/backend/access/gist/README b/src/backend/access/gist/README index 02228662b8..c84359de31 100644 --- a/src/backend/access/gist/README +++ b/src/backend/access/gist/README @@ -413,6 +413,20 @@ emptied yet; tuples never move upwards in the tree. The final emptying loops through buffers at a given level until all buffers at that level have been emptied, and then moves down to the next level. +Bulk delete algorithm (VACUUM) +------------------------------ + +Function gistbulkdelete() is responsible for marking empty leaf pages as free +so that they can be used it allocate newly split pages. To find this pages +function scans index in physical order. + +Physical scan reads the entire index from the first page to last. This scan +maintains information necessary to collect block numbers of internal pages +that need cleansing and block number of empty leafs. + +After the scan, for each internal pages under exclusive lock, each potentially +free leaf page is examined. gistbulkdelete() never delete last one reference +from internal page to preserve balanced tree properties. Authors: Teodor Sigaev diff --git a/src/backend/access/gist/gist.c b/src/backend/access/gist/gist.c index 2ce5425ef9..6f87b4b504 100644 --- a/src/backend/access/gist/gist.c +++ b/src/backend/access/gist/gist.c @@ -704,6 +704,11 @@ gistdoinsert(Relation r, IndexTuple itup, Size freespace, GISTInsertStack *item; OffsetNumber downlinkoffnum; + /* + * Currently internal pages are not deleted during vacuum, + * so we do not need to check if page is deleted + */ + downlinkoffnum = gistchoose(state.r, stack->page, itup, giststate); iid = PageGetItemId(stack->page, downlinkoffnum); idxtuple = (IndexTuple) PageGetItem(stack->page, iid); @@ -838,6 +843,19 @@ gistdoinsert(Relation r, IndexTuple itup, Size freespace, } } + /* + * Leaf pages can be left deleted but still referenced + * until it's space is reused. Downlink to this page may be already + * removed from the internal page, but this scan can posess it. + */ + if(GistPageIsDeleted(stack->page)) + { + UnlockReleaseBuffer(stack->buffer); + xlocked = false; + state.stack = stack = stack->parent; + continue; + } + /* now state.stack->(page, buffer and blkno) points to leaf page */ gistinserttuple(&state, stack, giststate, itup, diff --git a/src/backend/access/gist/gistutil.c b/src/backend/access/gist/gistutil.c index f32e16eed5..4fa44bf2f6 100644 --- a/src/backend/access/gist/gistutil.c +++ b/src/backend/access/gist/gistutil.c @@ -23,6 +23,7 @@ #include "storage/lmgr.h" #include "utils/float.h" #include "utils/syscache.h" +#include "utils/snapmgr.h" #include "utils/lsyscache.h" @@ -834,7 +835,7 @@ gistNewBuffer(Relation r) gistcheckpage(r, buffer); - if (GistPageIsDeleted(page)) + if (GistPageIsDeleted(page) && TransactionIdPrecedes(GistPageGetDeleteXid(page), RecentGlobalXmin)) return buffer; /* OK to use */ LockBuffer(buffer, GIST_UNLOCK); diff --git a/src/backend/access/gist/gistvacuum.c b/src/backend/access/gist/gistvacuum.c index 3c1d75691e..85b9d7c219 100644 --- a/src/backend/access/gist/gistvacuum.c +++ b/src/backend/access/gist/gistvacuum.c @@ -16,11 +16,15 @@ #include "access/genam.h" #include "access/gist_private.h" +#include "access/transam.h" #include "commands/vacuum.h" +#include "lib/blockset.h" #include "miscadmin.h" +#include "nodes/bitmapset.h" #include "storage/indexfsm.h" #include "storage/lmgr.h" + /* Working state needed by gistbulkdelete */ typedef struct { @@ -30,6 +34,10 @@ typedef struct void *callback_state; GistNSN startNSN; BlockNumber totFreePages; /* true total # of free pages */ + BlockNumber emptyPages; + + BlockSet internalPagesMap; + BlockSet emptyLeafPagesMap; } GistVacState; static void gistvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, @@ -50,6 +58,7 @@ gistbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, gistvacuumscan(info, stats, callback, callback_state); + return stats; } @@ -89,6 +98,57 @@ gistvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats) return stats; } +/* + * gistdeletepage takes parent page and leaf page and tries to delete leaf + * Both pages must be locked. Returns true if delete actually happened. + * Does not remove last downlink. + */ +static bool +gistdeletepage(GistVacState *vstate, + Buffer buffer, Page page, OffsetNumber downlink, + Buffer leafBuffer, Page leafPage, TransactionId txid) +{ + ItemId iid; + IndexTuple idxtuple; + XLogRecPtr recptr; + if (PageIsNew(page) || GistPageIsDeleted(page) || GistPageIsLeaf(page) + || PageGetMaxOffsetNumber(page) < downlink + || PageGetMaxOffsetNumber(page) <= FirstOffsetNumber) + return false; + + /* chech that old downlink is still pointing to leafBuffer */ + iid = PageGetItemId(page, downlink); + idxtuple = (IndexTuple) PageGetItem(page, iid); + if (BufferGetBlockNumber(leafBuffer) != + ItemPointerGetBlockNumber(&(idxtuple->t_tid))) + return false; + + /* Mark page as deleted dropping references from internal pages */ + START_CRIT_SECTION(); + + /* Remember xid of last transaction that could see this page */ + GistPageSetDeleteXid(leafPage,txid); + GistPageSetDeleted(leafPage); + MarkBufferDirty(leafBuffer); + vstate->stats->pages_deleted++; + vstate->emptyPages--; + + MarkBufferDirty(buffer); + /* Offsets are changed as long as we delete tuples from internal page */ + PageIndexTupleDelete(page, downlink); + + if (RelationNeedsWAL(vstate->info->index)) + recptr = gistXLogSetDeleted(vstate->info->index->rd_node, leafBuffer, + txid, buffer, downlink); + else + recptr = gistGetFakeLSN(vstate->info->index); + PageSetLSN(page, recptr); + PageSetLSN(leafPage, recptr); + + END_CRIT_SECTION(); + return true; +} + /* * gistvacuumscan --- scan the index for VACUUMing purposes * @@ -103,6 +163,11 @@ gistvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats) * while the index is being expanded, leaving an all-zeros page behind. * * The caller is responsible for initially allocating/zeroing a stats struct. + * + * Bulk deletion of all index entries pointing to a set of heap tuples and + * check invalid tuples left after upgrade. + * The set of target tuples is specified via a callback routine that tells + * whether any given heap tuple (identified by ItemPointer) is being deleted. */ static void gistvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, @@ -132,6 +197,9 @@ gistvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, else vstate.startNSN = gistGetFakeLSN(rel); vstate.totFreePages = 0; + vstate.emptyPages = 0; + vstate.internalPagesMap = NULL; + vstate.emptyLeafPagesMap = NULL; /* * The outer loop iterates over all index pages, in physical order (we @@ -171,6 +239,7 @@ gistvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, /* 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++) gistvacuumpage(&vstate, blkno, blkno); @@ -194,6 +263,111 @@ gistvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, /* update statistics */ stats->num_pages = num_pages; stats->pages_free = vstate.totFreePages; + + /* rescan all inner pages to find those that has empty child pages */ + if (vstate.emptyPages > 0) + { + BlockNumber x; + + x = InvalidBlockNumber; + while (vstate.emptyPages > 0 && + (x = blockset_next(vstate.internalPagesMap, x)) != InvalidBlockNumber) + { + Buffer buffer; + Page page; + OffsetNumber off, + maxoff; + IndexTuple idxtuple; + ItemId iid; + OffsetNumber todelete[MaxOffsetNumber]; + Buffer buftodelete[MaxOffsetNumber]; + int ntodelete = 0; + + blkno = (BlockNumber) x; + + buffer = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, + info->strategy); + + LockBuffer(buffer, GIST_EXCLUSIVE); + page = (Page) BufferGetPage(buffer); + if (PageIsNew(page) || GistPageIsDeleted(page) || GistPageIsLeaf(page)) + { + UnlockReleaseBuffer(buffer); + continue; + } + + maxoff = PageGetMaxOffsetNumber(page); + /* Check that leafs are still empty and decide what to delete */ + for (off = FirstOffsetNumber; off <= maxoff && ntodelete < maxoff-1; off = OffsetNumberNext(off)) + { + Buffer leafBuffer; + BlockNumber leafBlockNo; + + /* We must keep at least one leaf page per each */ + if (ntodelete >= maxoff-1) + continue; + + iid = PageGetItemId(page, off); + idxtuple = (IndexTuple) PageGetItem(page, iid); + /* if this page was not empty in previous scan - we do not consider it */ + leafBlockNo = ItemPointerGetBlockNumber(&(idxtuple->t_tid)); + if (!blockset_get(leafBlockNo, vstate.emptyLeafPagesMap)) + continue; + + leafBuffer = ReadBufferExtended(rel, MAIN_FORKNUM, leafBlockNo, + RBM_NORMAL, info->strategy); + + buftodelete[ntodelete] = leafBuffer; + todelete[ntodelete++] = off; + } + + /* + * We will have to relock internal page in case of deletes: + * we cannot lock child while holding parent lock without risk + * of a deadlock + */ + LockBuffer(buffer, GIST_UNLOCK); + + if (ntodelete) + { + /* + * Like in _bt_unlink_halfdead_page we need an upper bound on xid + * that could hold downlinks to this page. We use + * ReadNewTransactionId() to instead of GetCurrentTransactionId + * since we are in a VACUUM. + */ + TransactionId txid = ReadNewTransactionId(); + + int deleted = 0; + + for (off = 0; off < ntodelete; off++) + { + Buffer leafBuffer = buftodelete[off]; + Page leafPage; + LockBuffer(leafBuffer, GIST_EXCLUSIVE); + gistcheckpage(rel, leafBuffer); + leafPage = (Page) BufferGetPage(leafBuffer); + if (GistPageIsLeaf(leafPage) /* not a leaf anymore */ + && PageGetMaxOffsetNumber(leafPage) == InvalidOffsetNumber /* Page is not empry */ + && !(GistFollowRight(leafPage) || GistPageGetNSN(page) > GistPageGetNSN(leafPage)) /* No follow-right */ + ) + { + LockBuffer(buffer, GIST_EXCLUSIVE); + page = (Page) BufferGetPage(buffer); + if (gistdeletepage(&vstate, buffer, page, todelete[off] - deleted, leafBuffer, leafPage, txid)) + deleted++; + LockBuffer(buffer, GIST_UNLOCK); + } + UnlockReleaseBuffer(leafBuffer); + } + } + + ReleaseBuffer(buffer); + } + } + + blockset_free(vstate.emptyLeafPagesMap); + blockset_free(vstate.internalPagesMap); } /* @@ -246,6 +420,7 @@ restart: { OffsetNumber todelete[MaxOffsetNumber]; int ntodelete = 0; + int nremain; GISTPageOpaque opaque = GistPageGetOpaque(page); OffsetNumber maxoff = PageGetMaxOffsetNumber(page); @@ -319,10 +494,19 @@ restart: maxoff = PageGetMaxOffsetNumber(page); } - stats->num_index_tuples += maxoff - FirstOffsetNumber + 1; + nremain = maxoff - FirstOffsetNumber + 1; + if (nremain == 0) + { + vstate->emptyLeafPagesMap = blockset_set(vstate->emptyLeafPagesMap, blkno); + vstate->emptyPages++; + } + else + stats->num_index_tuples += nremain; } else { + vstate->internalPagesMap = blockset_set(vstate->internalPagesMap, blkno); + /* * On an internal page, check for "invalid tuples", left behind by an * incomplete page split on PostgreSQL 9.0 or below. These are not diff --git a/src/backend/access/gist/gistxlog.c b/src/backend/access/gist/gistxlog.c index 408bd5390a..3213ea98ea 100644 --- a/src/backend/access/gist/gistxlog.c +++ b/src/backend/access/gist/gistxlog.c @@ -64,6 +64,39 @@ gistRedoClearFollowRight(XLogReaderState *record, uint8 block_id) UnlockReleaseBuffer(buffer); } +static void +gistRedoPageSetDeleted(XLogReaderState *record) +{ + XLogRecPtr lsn = record->EndRecPtr; + gistxlogPageDelete *xldata = (gistxlogPageDelete *) XLogRecGetData(record); + Buffer buffer; + Page page; + + if (XLogReadBufferForRedo(record, 0, &buffer) == BLK_NEEDS_REDO) + { + page = (Page) BufferGetPage(buffer); + + GistPageSetDeleteXid(page, xldata->deleteXid); + GistPageSetDeleted(page); + + PageSetLSN(page, lsn); + MarkBufferDirty(buffer); + } + if (BufferIsValid(buffer)) + UnlockReleaseBuffer(buffer); + + if (XLogReadBufferForRedo(record, 1, &buffer) == BLK_NEEDS_REDO) + { + page = (Page) BufferGetPage(buffer); + + PageIndexTupleDelete(page, xldata->downlinkOffset); + + PageSetLSN(page, lsn); + MarkBufferDirty(buffer); + } + if (BufferIsValid(buffer)) + UnlockReleaseBuffer(buffer); +} /* * redo any page update (except page split) */ @@ -116,6 +149,7 @@ gistRedoPageUpdateRecord(XLogReaderState *record) data += sizeof(OffsetNumber) * xldata->ntodelete; PageIndexMultiDelete(page, todelete, xldata->ntodelete); + if (GistPageIsLeaf(page)) GistMarkTuplesDeleted(page); } @@ -535,6 +569,9 @@ gist_redo(XLogReaderState *record) case XLOG_GIST_CREATE_INDEX: gistRedoCreateIndex(record); break; + case XLOG_GIST_PAGE_DELETE: + gistRedoPageSetDeleted(record); + break; default: elog(PANIC, "gist_redo: unknown op code %u", info); } @@ -653,6 +690,29 @@ gistXLogSplit(bool page_is_leaf, return recptr; } +/* + * Write XLOG record describing a page delete. This also includes removal of + * downlink from internal page. + */ +XLogRecPtr +gistXLogSetDeleted(RelFileNode node, Buffer buffer, TransactionId xid, + Buffer internalPageBuffer, OffsetNumber internalPageOffset) { + gistxlogPageDelete xlrec; + XLogRecPtr recptr; + + xlrec.deleteXid = xid; + xlrec.downlinkOffset = internalPageOffset; + + XLogBeginInsert(); + XLogRegisterData((char *) &xlrec, sizeof(gistxlogPageDelete)); + + XLogRegisterBuffer(0, buffer, REGBUF_STANDARD); + XLogRegisterBuffer(1, internalPageBuffer, REGBUF_STANDARD); + /* new tuples */ + recptr = XLogInsert(RM_GIST_ID, XLOG_GIST_PAGE_DELETE); + return recptr; +} + /* * Write XLOG record describing a page update. The update can include any * number of deletions and/or insertions of tuples on a single index page. diff --git a/src/backend/access/rmgrdesc/gistdesc.c b/src/backend/access/rmgrdesc/gistdesc.c index e468c9e15a..0861f82992 100644 --- a/src/backend/access/rmgrdesc/gistdesc.c +++ b/src/backend/access/rmgrdesc/gistdesc.c @@ -76,6 +76,9 @@ gist_identify(uint8 info) case XLOG_GIST_CREATE_INDEX: id = "CREATE_INDEX"; break; + case XLOG_GIST_PAGE_DELETE: + id = "PAGE_DELETE"; + break; } return id; diff --git a/src/include/access/gist.h b/src/include/access/gist.h index 3234f24156..ce8bfd83ea 100644 --- a/src/include/access/gist.h +++ b/src/include/access/gist.h @@ -151,6 +151,10 @@ typedef struct GISTENTRY #define GistPageGetNSN(page) ( PageXLogRecPtrGet(GistPageGetOpaque(page)->nsn)) #define GistPageSetNSN(page, val) ( PageXLogRecPtrSet(GistPageGetOpaque(page)->nsn, val)) +/* For deleted pages we store last xid which could see the page in scan */ +#define GistPageGetDeleteXid(page) ( ((PageHeader) (page))->pd_prune_xid ) +#define GistPageSetDeleteXid(page, val) ( ((PageHeader) (page))->pd_prune_xid = val) + /* * Vector of GISTENTRY structs; user-defined methods union and picksplit * take it as one of their arguments diff --git a/src/include/access/gist_private.h b/src/include/access/gist_private.h index 463d2bfc7b..943163ccce 100644 --- a/src/include/access/gist_private.h +++ b/src/include/access/gist_private.h @@ -414,12 +414,17 @@ extern bool gistplacetopage(Relation rel, Size freespace, GISTSTATE *giststate, extern SplitedPageLayout *gistSplit(Relation r, Page page, IndexTuple *itup, int len, GISTSTATE *giststate); +/* gistxlog.c */ +extern XLogRecPtr gistXLogSetDeleted(RelFileNode node, Buffer buffer, + TransactionId xid, Buffer internalPageBuffer, + OffsetNumber internalPageOffset); + extern XLogRecPtr gistXLogUpdate(Buffer buffer, OffsetNumber *todelete, int ntodelete, IndexTuple *itup, int ntup, Buffer leftchild); -XLogRecPtr gistXLogDelete(Buffer buffer, OffsetNumber *todelete, +extern XLogRecPtr gistXLogDelete(Buffer buffer, OffsetNumber *todelete, int ntodelete, RelFileNode hnode); extern XLogRecPtr gistXLogSplit(bool page_is_leaf, diff --git a/src/include/access/gistxlog.h b/src/include/access/gistxlog.h index 5117aabf1a..127cff5cb7 100644 --- a/src/include/access/gistxlog.h +++ b/src/include/access/gistxlog.h @@ -17,13 +17,15 @@ #include "access/xlogreader.h" #include "lib/stringinfo.h" +/* XLog stuff */ + #define XLOG_GIST_PAGE_UPDATE 0x00 #define XLOG_GIST_DELETE 0x10 /* delete leaf index tuples for a page */ /* #define XLOG_GIST_NEW_ROOT 0x20 */ /* not used anymore */ #define XLOG_GIST_PAGE_SPLIT 0x30 /* #define XLOG_GIST_INSERT_COMPLETE 0x40 */ /* not used anymore */ #define XLOG_GIST_CREATE_INDEX 0x50 - /* #define XLOG_GIST_PAGE_DELETE 0x60 */ /* not used anymore */ +#define XLOG_GIST_PAGE_DELETE 0x60 /* * Backup Blk 0: updated page. @@ -76,6 +78,12 @@ typedef struct gistxlogPageSplit */ } gistxlogPageSplit; +typedef struct gistxlogPageDelete +{ + TransactionId deleteXid; /* last Xid which could see page in scan */ + OffsetNumber downlinkOffset; /* Offset of the downlink referencing this page */ +} gistxlogPageDelete; + extern void gist_redo(XLogReaderState *record); extern void gist_desc(StringInfo buf, XLogReaderState *record); extern const char *gist_identify(uint8 info); diff --git a/src/test/regress/expected/gist.out b/src/test/regress/expected/gist.out index f5a2993aaf..0a43449f00 100644 --- a/src/test/regress/expected/gist.out +++ b/src/test/regress/expected/gist.out @@ -27,10 +27,8 @@ insert into gist_point_tbl (id, p) select g+100000, point(g*10+1, g*10+1) from generate_series(1, 10000) g; -- To test vacuum, delete some entries from all over the index. delete from gist_point_tbl where id % 2 = 1; --- And also delete some concentration of values. (GiST doesn't currently --- attempt to delete pages even when they become empty, but if it did, this --- would exercise it) -delete from gist_point_tbl where id < 10000; +-- And also delete some concentration of values. +delete from gist_point_tbl where id > 5000; vacuum analyze gist_point_tbl; -- rebuild the index with a different fillfactor alter index gist_pointidx SET (fillfactor = 40); diff --git a/src/test/regress/sql/gist.sql b/src/test/regress/sql/gist.sql index bae722fe13..657b195484 100644 --- a/src/test/regress/sql/gist.sql +++ b/src/test/regress/sql/gist.sql @@ -28,10 +28,8 @@ select g+100000, point(g*10+1, g*10+1) from generate_series(1, 10000) g; -- To test vacuum, delete some entries from all over the index. delete from gist_point_tbl where id % 2 = 1; --- And also delete some concentration of values. (GiST doesn't currently --- attempt to delete pages even when they become empty, but if it did, this --- would exercise it) -delete from gist_point_tbl where id < 10000; +-- And also delete some concentration of values. +delete from gist_point_tbl where id > 5000; vacuum analyze gist_point_tbl; -- 2.20.1