Deprecate nbtree's BTP_HAS_GARBAGE flag.
authorPeter Geoghegan <[email protected]>
Tue, 17 Nov 2020 17:45:56 +0000 (09:45 -0800)
committerPeter Geoghegan <[email protected]>
Tue, 17 Nov 2020 17:45:56 +0000 (09:45 -0800)
Streamline handling of the various strategies that we have to avoid a
page split in nbtinsert.c.  When it looks like a leaf page is about to
overflow, we now perform deleting LP_DEAD items and deduplication in one
central place.  This greatly simplifies _bt_findinsertloc().

This has an independently useful consequence: nbtree no longer relies on
the BTP_HAS_GARBAGE page level flag/hint for anything important.  We
still set and unset the flag in the same way as before, but it's no
longer treated as a gating condition when considering if we should check
for already-set LP_DEAD bits.  This happens at the point where the page
looks like it might have to be split anyway, so simply checking the
LP_DEAD bits in passing is practically free.  This avoids missing
LP_DEAD bits just because the page-level hint is unset, which is
probably reasonably common (e.g. it happens when VACUUM unsets the
page-level flag without actually removing index tuples whose LP_DEAD-bit
was set recently, after the VACUUM operation began but before it reached
the leaf page in question).

Note that this isn't a big behavioral change compared to PostgreSQL 13.
We were already checking for set LP_DEAD bits regardless of whether the
BTP_HAS_GARBAGE page level flag was set before we considered doing a
deduplication pass.  This commit only goes slightly further by doing the
same check for all indexes, even indexes where deduplication won't be
performed.

We don't completely remove the BTP_HAS_GARBAGE flag.  We still rely on
it as a gating condition with pg_upgrade'd indexes from before B-tree
version 4/PostgreSQL 12.  That makes sense because we sometimes have to
make a choice among pages full of duplicates when inserting a tuple with
pre version 4 indexes.  It probably still pays to avoid accessing the
line pointer array of a page there, since it won't yet be clear whether
we'll insert on to the page in question at all, let alone split it as a
result.

Author: Peter Geoghegan <[email protected]>
Reviewed-By: Victor Yegorov <[email protected]>
Discussion: https://postgr.es/m/CAH2-Wz%3DYpc1PDdk8OVJDChGJBjT06%3DA0Mbv9HyTLCsOknGcUFg%40mail.gmail.com

src/backend/access/nbtree/nbtdedup.c
src/backend/access/nbtree/nbtinsert.c
src/backend/access/nbtree/nbtpage.c
src/backend/access/nbtree/nbtutils.c
src/backend/access/nbtree/nbtxlog.c
src/include/access/nbtree.h

index f6be865b17e3635352600ace47c58c50a42a3fa9..9e535124c46523ee1e7eb49e048c5920975c975f 100644 (file)
@@ -28,9 +28,7 @@ static bool _bt_posting_valid(IndexTuple posting);
 #endif
 
 /*
- * Deduplicate items on a leaf page.  The page will have to be split by caller
- * if we cannot successfully free at least newitemsz (we also need space for
- * newitem's line pointer, which isn't included in caller's newitemsz).
+ * Perform a deduplication pass.
  *
  * The general approach taken here is to perform as much deduplication as
  * possible to free as much space as possible.  Note, however, that "single
@@ -43,76 +41,32 @@ static bool _bt_posting_valid(IndexTuple posting);
  * handle those if and when the anticipated right half page gets its own
  * deduplication pass, following further inserts of duplicates.)
  *
- * This function should be called during insertion, when the page doesn't have
- * enough space to fit an incoming newitem.  If the BTP_HAS_GARBAGE page flag
- * was set, caller should have removed any LP_DEAD items by calling
- * _bt_vacuum_one_page() before calling here.  We may still have to kill
- * LP_DEAD items here when the page's BTP_HAS_GARBAGE hint is falsely unset,
- * but that should be rare.  Also, _bt_vacuum_one_page() won't unset the
- * BTP_HAS_GARBAGE flag when it finds no LP_DEAD items, so a successful
- * deduplication pass will always clear it, just to keep things tidy.
+ * The page will have to be split if we cannot successfully free at least
+ * newitemsz (we also need space for newitem's line pointer, which isn't
+ * included in caller's newitemsz).
+ *
+ * Note: Caller should have already deleted all existing items with their
+ * LP_DEAD bits set.
  */
 void
-_bt_dedup_one_page(Relation rel, Buffer buf, Relation heapRel,
-                  IndexTuple newitem, Size newitemsz, bool checkingunique)
+_bt_dedup_pass(Relation rel, Buffer buf, Relation heapRel, IndexTuple newitem,
+              Size newitemsz, bool checkingunique)
 {
    OffsetNumber offnum,
                minoff,
                maxoff;
    Page        page = BufferGetPage(buf);
-   BTPageOpaque opaque;
+   BTPageOpaque opaque = (BTPageOpaque) PageGetSpecialPointer(page);
    Page        newpage;
-   OffsetNumber deletable[MaxIndexTuplesPerPage];
    BTDedupState state;
-   int         ndeletable = 0;
    Size        pagesaving = 0;
    bool        singlevalstrat = false;
    int         nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel);
 
-   /*
-    * We can't assume that there are no LP_DEAD items.  For one thing, VACUUM
-    * will clear the BTP_HAS_GARBAGE hint without reliably removing items
-    * that are marked LP_DEAD.  We don't want to unnecessarily unset LP_DEAD
-    * bits when deduplicating items.  Allowing it would be correct, though
-    * wasteful.
-    */
-   opaque = (BTPageOpaque) PageGetSpecialPointer(page);
-   minoff = P_FIRSTDATAKEY(opaque);
-   maxoff = PageGetMaxOffsetNumber(page);
-   for (offnum = minoff;
-        offnum <= maxoff;
-        offnum = OffsetNumberNext(offnum))
-   {
-       ItemId      itemid = PageGetItemId(page, offnum);
-
-       if (ItemIdIsDead(itemid))
-           deletable[ndeletable++] = offnum;
-   }
-
-   if (ndeletable > 0)
-   {
-       _bt_delitems_delete(rel, buf, deletable, ndeletable, heapRel);
-
-       /*
-        * Return when a split will be avoided.  This is equivalent to
-        * avoiding a split using the usual _bt_vacuum_one_page() path.
-        */
-       if (PageGetFreeSpace(page) >= newitemsz)
-           return;
-
-       /*
-        * Reconsider number of items on page, in case _bt_delitems_delete()
-        * managed to delete an item or two
-        */
-       minoff = P_FIRSTDATAKEY(opaque);
-       maxoff = PageGetMaxOffsetNumber(page);
-   }
-
    /* Passed-in newitemsz is MAXALIGNED but does not include line pointer */
    newitemsz += sizeof(ItemIdData);
 
    /*
-    * By here, it's clear that deduplication will definitely be attempted.
     * Initialize deduplication state.
     *
     * It would be possible for maxpostingsize (limit on posting list tuple
@@ -138,6 +92,9 @@ _bt_dedup_one_page(Relation rel, Buffer buf, Relation heapRel,
    /* nintervals should be initialized to zero */
    state->nintervals = 0;
 
+   minoff = P_FIRSTDATAKEY(opaque);
+   maxoff = PageGetMaxOffsetNumber(page);
+
    /* Determine if "single value" strategy should be used */
    if (!checkingunique)
        singlevalstrat = _bt_do_singleval(rel, page, state, minoff, newitem);
@@ -259,10 +216,9 @@ _bt_dedup_one_page(Relation rel, Buffer buf, Relation heapRel,
    /*
     * By here, it's clear that deduplication will definitely go ahead.
     *
-    * Clear the BTP_HAS_GARBAGE page flag in the unlikely event that it is
-    * still falsely set, just to keep things tidy.  (We can't rely on
-    * _bt_vacuum_one_page() having done this already, and we can't rely on a
-    * page split or VACUUM getting to it in the near future.)
+    * Clear the BTP_HAS_GARBAGE page flag.  The index must be a heapkeyspace
+    * index, and as such we'll never pay attention to BTP_HAS_GARBAGE anyway.
+    * But keep things tidy.
     */
    if (P_HAS_GARBAGE(opaque))
    {
index b6ddf0ec4caae997694c40333be8f187ab508259..dde43b1415ac1111a5198667f3cdff832618c759 100644 (file)
@@ -58,7 +58,10 @@ static void _bt_insert_parent(Relation rel, Buffer buf, Buffer rbuf,
 static Buffer _bt_newroot(Relation rel, Buffer lbuf, Buffer rbuf);
 static inline bool _bt_pgaddtup(Page page, Size itemsize, IndexTuple itup,
                                OffsetNumber itup_off, bool newfirstdataitem);
-static void _bt_vacuum_one_page(Relation rel, Buffer buffer, Relation heapRel);
+static void _bt_delete_or_dedup_one_page(Relation rel, Relation heapRel,
+                                        BTInsertState insertstate,
+                                        bool lpdeadonly, bool checkingunique,
+                                        bool uniquedup);
 
 /*
  * _bt_doinsert() -- Handle insertion of a single index tuple in the tree.
@@ -871,38 +874,14 @@ _bt_findinsertloc(Relation rel,
        }
 
        /*
-        * If the target page is full, see if we can obtain enough space by
-        * erasing LP_DEAD items.  If that fails to free enough space, see if
-        * we can avoid a page split by performing a deduplication pass over
-        * the page.
-        *
-        * We only perform a deduplication pass for a checkingunique caller
-        * when the incoming item is a duplicate of an existing item on the
-        * leaf page.  This heuristic avoids wasting cycles -- we only expect
-        * to benefit from deduplicating a unique index page when most or all
-        * recently added items are duplicates.  See nbtree/README.
+        * If the target page is full, see if we can obtain enough space using
+        * one or more strategies (e.g. erasing LP_DEAD items, deduplication).
+        * Page splits are expensive, and should only go ahead when truly
+        * necessary.
         */
        if (PageGetFreeSpace(page) < insertstate->itemsz)
-       {
-           if (P_HAS_GARBAGE(opaque))
-           {
-               _bt_vacuum_one_page(rel, insertstate->buf, heapRel);
-               insertstate->bounds_valid = false;
-
-               /* Might as well assume duplicates (if checkingunique) */
-               uniquedup = true;
-           }
-
-           if (itup_key->allequalimage && BTGetDeduplicateItems(rel) &&
-               (!checkingunique || uniquedup) &&
-               PageGetFreeSpace(page) < insertstate->itemsz)
-           {
-               _bt_dedup_one_page(rel, insertstate->buf, heapRel,
-                                  insertstate->itup, insertstate->itemsz,
-                                  checkingunique);
-               insertstate->bounds_valid = false;
-           }
-       }
+           _bt_delete_or_dedup_one_page(rel, heapRel, insertstate, false,
+                                        checkingunique, uniquedup);
    }
    else
    {
@@ -942,8 +921,9 @@ _bt_findinsertloc(Relation rel,
             */
            if (P_HAS_GARBAGE(opaque))
            {
-               _bt_vacuum_one_page(rel, insertstate->buf, heapRel);
-               insertstate->bounds_valid = false;
+               /* Erase LP_DEAD items (won't deduplicate) */
+               _bt_delete_or_dedup_one_page(rel, heapRel, insertstate, true,
+                                            checkingunique, false);
 
                if (PageGetFreeSpace(page) >= insertstate->itemsz)
                    break;      /* OK, now we have enough space */
@@ -993,14 +973,17 @@ _bt_findinsertloc(Relation rel,
         * performing a posting list split, so delete all LP_DEAD items early.
         * This is the only case where LP_DEAD deletes happen even though
         * there is space for newitem on the page.
+        *
+        * This can only erase LP_DEAD items (it won't deduplicate).
         */
-       _bt_vacuum_one_page(rel, insertstate->buf, heapRel);
+       _bt_delete_or_dedup_one_page(rel, heapRel, insertstate, true,
+                                    checkingunique, false);
 
        /*
         * Do new binary search.  New insert location cannot overlap with any
         * posting list now.
         */
-       insertstate->bounds_valid = false;
+       Assert(!insertstate->bounds_valid);
        insertstate->postingoff = 0;
        newitemoff = _bt_binsrch_insert(rel, insertstate);
        Assert(insertstate->postingoff == 0);
@@ -2623,32 +2606,59 @@ _bt_pgaddtup(Page page,
 }
 
 /*
- * _bt_vacuum_one_page - vacuum just one index page.
+ * _bt_delete_or_dedup_one_page - Try to avoid a leaf page split by attempting
+ * a variety of operations.
+ *
+ * There are two operations performed here: deleting items already marked
+ * LP_DEAD, and deduplication.  If both operations fail to free enough space
+ * for the incoming item then caller will go on to split the page.  We always
+ * attempt our preferred strategy (which is to delete items whose LP_DEAD bit
+ * are set) first.  If that doesn't work out we move on to deduplication.
+ *
+ * Caller's checkingunique and uniquedup arguments help us decide if we should
+ * perform deduplication, which is primarily useful with low cardinality data,
+ * but can sometimes absorb version churn.
+ *
+ * Callers that only want us to look for/delete LP_DEAD items can ask for that
+ * directly by passing true 'lpdeadonly' argument.
  *
- * Try to remove LP_DEAD items from the given page.  The passed buffer
- * must be exclusive-locked, but unlike a real VACUUM, we don't need a
- * super-exclusive "cleanup" lock (see nbtree/README).
+ * Note: We used to only delete LP_DEAD items when the BTP_HAS_GARBAGE page
+ * level flag was found set.  The flag was useful back when there wasn't
+ * necessarily one single page for a duplicate tuple to go on (before heap TID
+ * became a part of the key space in version 4 indexes).  But we don't
+ * actually look at the flag anymore (it's not a gating condition for our
+ * caller).  That would cause us to miss tuples that are safe to delete,
+ * without getting any benefit in return.  We know that the alternative is to
+ * split the page; scanning the line pointer array in passing won't have
+ * noticeable overhead.  (We still maintain the BTP_HAS_GARBAGE flag despite
+ * all this because !heapkeyspace indexes must still do a "getting tired"
+ * linear search, and so are likely to get some benefit from using it as a
+ * gating condition.)
  */
 static void
-_bt_vacuum_one_page(Relation rel, Buffer buffer, Relation heapRel)
+_bt_delete_or_dedup_one_page(Relation rel, Relation heapRel,
+                            BTInsertState insertstate,
+                            bool lpdeadonly, bool checkingunique,
+                            bool uniquedup)
 {
    OffsetNumber deletable[MaxIndexTuplesPerPage];
    int         ndeletable = 0;
    OffsetNumber offnum,
-               minoff,
                maxoff;
+   Buffer      buffer = insertstate->buf;
+   BTScanInsert itup_key = insertstate->itup_key;
    Page        page = BufferGetPage(buffer);
    BTPageOpaque opaque = (BTPageOpaque) PageGetSpecialPointer(page);
 
    Assert(P_ISLEAF(opaque));
+   Assert(lpdeadonly || itup_key->heapkeyspace);
 
    /*
     * Scan over all items to see which ones need to be deleted according to
     * LP_DEAD flags.
     */
-   minoff = P_FIRSTDATAKEY(opaque);
    maxoff = PageGetMaxOffsetNumber(page);
-   for (offnum = minoff;
+   for (offnum = P_FIRSTDATAKEY(opaque);
         offnum <= maxoff;
         offnum = OffsetNumberNext(offnum))
    {
@@ -2659,12 +2669,50 @@ _bt_vacuum_one_page(Relation rel, Buffer buffer, Relation heapRel)
    }
 
    if (ndeletable > 0)
+   {
        _bt_delitems_delete(rel, buffer, deletable, ndeletable, heapRel);
+       insertstate->bounds_valid = false;
+
+       /* Return when a page split has already been avoided */
+       if (PageGetFreeSpace(page) >= insertstate->itemsz)
+           return;
+
+       /* Might as well assume duplicates (if checkingunique) */
+       uniquedup = true;
+   }
+
+   /*
+    * Some callers only want to delete LP_DEAD items.  Return early for these
+    * callers.
+    *
+    * Note: The page's BTP_HAS_GARBAGE hint flag may still be set when we
+    * return at this point (or when we go on the try either or both of our
+    * other strategies and they also fail).  We do not bother expending a
+    * separate write to clear it, however.  Caller will definitely clear it
+    * when it goes on to split the page (plus deduplication knows to clear
+    * the flag when it actually modifies the page).
+    */
+   if (lpdeadonly)
+       return;
+
+   /*
+    * We can get called in the checkingunique case when there is no reason to
+    * believe that there are any duplicates on the page; we should at least
+    * still check for LP_DEAD items.  If that didn't work out, give up and
+    * let caller split the page.  Deduplication cannot be justified given
+    * there is no reason to think that there are duplicates.
+    */
+   if (checkingunique && !uniquedup)
+       return;
+
+   /* Assume bounds about to be invalidated (this is almost certain now) */
+   insertstate->bounds_valid = false;
 
    /*
-    * Note: if we didn't find any LP_DEAD items, then the page's
-    * BTP_HAS_GARBAGE hint bit is falsely set.  We do not bother expending a
-    * separate write to clear it, however.  We will clear it when we split
-    * the page, or when deduplication runs.
+    * Perform deduplication pass, though only when it is enabled for the
+    * index and known to be safe (it must be an allequalimage index).
     */
+   if (BTGetDeduplicateItems(rel) && itup_key->allequalimage)
+       _bt_dedup_pass(rel, buffer, heapRel, insertstate->itup,
+                      insertstate->itemsz, checkingunique);
 }
index 7f392480ac0f2ed21d045ad0934c5e36a1f1237b..848123d921838d401d4ba757bfc0c7dfab43aa1a 100644 (file)
@@ -1187,8 +1187,8 @@ _bt_delitems_vacuum(Relation rel, Buffer buf,
     * array of offset numbers.
     *
     * PageIndexTupleOverwrite() won't unset each item's LP_DEAD bit when it
-    * happens to already be set.  Although we unset the BTP_HAS_GARBAGE page
-    * level flag, unsetting individual LP_DEAD bits should still be avoided.
+    * happens to already be set.  It's important that we not interfere with
+    * _bt_delitems_delete().
     */
    for (int i = 0; i < nupdatable; i++)
    {
@@ -1215,20 +1215,12 @@ _bt_delitems_vacuum(Relation rel, Buffer buf,
    opaque->btpo_cycleid = 0;
 
    /*
-    * Mark the page as not containing any LP_DEAD items.  This is not
-    * certainly true (there might be some that have recently been marked, but
-    * weren't targeted by VACUUM's heap scan), but it will be true often
-    * enough.  VACUUM does not delete items purely because they have their
-    * LP_DEAD bit set, since doing so would necessitate explicitly logging a
-    * latestRemovedXid cutoff (this is how _bt_delitems_delete works).
+    * Clear the BTP_HAS_GARBAGE page flag.
     *
-    * The consequences of falsely unsetting BTP_HAS_GARBAGE should be fairly
-    * limited, since we never falsely unset an LP_DEAD bit.  Workloads that
-    * are particularly dependent on LP_DEAD bits being set quickly will
-    * usually manage to set the BTP_HAS_GARBAGE flag before the page fills up
-    * again anyway.  Furthermore, attempting a deduplication pass will remove
-    * all LP_DEAD items, regardless of whether the BTP_HAS_GARBAGE hint bit
-    * is set or not.
+    * This flag indicates the presence of LP_DEAD items on the page (though
+    * not reliably).  Note that we only trust it with pg_upgrade'd
+    * !heapkeyspace indexes.  That's why clearing it here won't usually
+    * interfere with _bt_delitems_delete().
     */
    opaque->btpo_flags &= ~BTP_HAS_GARBAGE;
 
@@ -1310,10 +1302,17 @@ _bt_delitems_delete(Relation rel, Buffer buf,
 
    /*
     * Unlike _bt_delitems_vacuum, we *must not* clear the vacuum cycle ID,
-    * because this is not called by VACUUM.  Just clear the BTP_HAS_GARBAGE
-    * page flag, since we deleted all items with their LP_DEAD bit set.
+    * because this is not called by VACUUM
     */
    opaque = (BTPageOpaque) PageGetSpecialPointer(page);
+
+   /*
+    * Clear the BTP_HAS_GARBAGE page flag.
+    *
+    * This flag indicates the presence of LP_DEAD items on the page (though
+    * not reliably).  Note that we only trust it with pg_upgrade'd
+    * !heapkeyspace indexes.
+    */
    opaque->btpo_flags &= ~BTP_HAS_GARBAGE;
 
    MarkBufferDirty(buf);
index 81589b9056dd3f053ace41a64410db65df32eae1..2f5f14e527dd06a2b5e51d0eeb26d520ea00f562 100644 (file)
@@ -1877,7 +1877,8 @@ _bt_killitems(IndexScanDesc scan)
     * Since this can be redone later if needed, mark as dirty hint.
     *
     * Whenever we mark anything LP_DEAD, we also set the page's
-    * BTP_HAS_GARBAGE flag, which is likewise just a hint.
+    * BTP_HAS_GARBAGE flag, which is likewise just a hint.  (Note that we
+    * only rely on the page-level flag in !heapkeyspace indexes.)
     */
    if (killedsomething)
    {
index e9132267604f431286a2eb2b1b8a9cced5afa438..5135b800af6d355bda2f2b10f36f5582781c5670 100644 (file)
@@ -1065,7 +1065,8 @@ btree_mask(char *pagedata, BlockNumber blkno)
 
    /*
     * BTP_HAS_GARBAGE is just an un-logged hint bit. So, mask it. See
-    * _bt_killitems(), _bt_check_unique() for details.
+    * _bt_delete_or_dedup_one_page(), _bt_killitems(), and _bt_check_unique()
+    * for details.
     */
    maskopaq->btpo_flags &= ~BTP_HAS_GARBAGE;
 
index 65d9698b899ac1b71448e2128db79e6b7b995479..e8fecc6026f3da769f47459643449d463138e0c1 100644 (file)
@@ -75,7 +75,7 @@ typedef BTPageOpaqueData *BTPageOpaque;
 #define BTP_META       (1 << 3)    /* meta-page */
 #define BTP_HALF_DEAD  (1 << 4)    /* empty, but still in tree */
 #define BTP_SPLIT_END  (1 << 5)    /* rightmost page of split group */
-#define BTP_HAS_GARBAGE (1 << 6)   /* page has LP_DEAD tuples */
+#define BTP_HAS_GARBAGE (1 << 6)   /* page has LP_DEAD tuples (deprecated) */
 #define BTP_INCOMPLETE_SPLIT (1 << 7)  /* right sibling's downlink is missing */
 
 /*
@@ -1027,9 +1027,9 @@ extern void _bt_parallel_advance_array_keys(IndexScanDesc scan);
 /*
  * prototypes for functions in nbtdedup.c
  */
-extern void _bt_dedup_one_page(Relation rel, Buffer buf, Relation heapRel,
-                              IndexTuple newitem, Size newitemsz,
-                              bool checkingunique);
+extern void _bt_dedup_pass(Relation rel, Buffer buf, Relation heapRel,
+                          IndexTuple newitem, Size newitemsz,
+                          bool checkingunique);
 extern void _bt_dedup_start_pending(BTDedupState state, IndexTuple base,
                                    OffsetNumber baseoff);
 extern bool _bt_dedup_save_htid(BTDedupState state, IndexTuple itup);