From 1a4e551010923073872ee02dd50990fbec02ec93 Mon Sep 17 00:00:00 2001 From: Peter Geoghegan Date: Mon, 1 Oct 2018 16:48:08 -0700 Subject: [PATCH v17 5/7] Add "split after new tuple" optimization. Add additional heuristics to the algorithm for locating an optimal split location. New logic identifies localized monotonically increasing values in indexes with multiple columns. When this insertion pattern is detected, page splits split just after the new item that provoked a page split (or apply leaf fillfactor in the style of a rightmost page split). This optimization is a variation of the long established leaf fillfactor optimization used during rightmost page splits. 50/50 page splits are only appropriate with a pattern of truly random insertions, where the average space utilization ends up at 65% - 70%. Without this patch, affected cases have leaf pages that are no more than about 50% full on average. Future insertions can never make use of the free space left behind. With this patch, affected cases have leaf pages that are about 90% full on average (assuming a fillfactor of 90). Localized monotonically increasing insertion patterns are presumed to be fairly common in real-world applications. Note that even pg_upgrade'd v3 indexes make use of this optimization. Author: Peter Geoghegan Reviewed-By: Heikki Linnakangas Discussion: https://postgr.es/m/CAH2-WzkpKeZJrXvR_p7VSY1b-s85E3gHyTbZQzR0BkJ5LrWF_A@mail.gmail.com --- src/backend/access/nbtree/nbtsplitloc.c | 234 +++++++++++++++++++++++- 1 file changed, 231 insertions(+), 3 deletions(-) diff --git a/src/backend/access/nbtree/nbtsplitloc.c b/src/backend/access/nbtree/nbtsplitloc.c index ead218d916..a25c41df68 100644 --- a/src/backend/access/nbtree/nbtsplitloc.c +++ b/src/backend/access/nbtree/nbtsplitloc.c @@ -70,6 +70,9 @@ static void _bt_recsplitloc(FindSplitData *state, static void _bt_deltasortsplits(FindSplitData *state, double fillfactormult, bool usemult); static int _bt_splitcmp(const void *arg1, const void *arg2); +static bool _bt_afternewitemoff(FindSplitData *state, OffsetNumber maxoff, + int leaffillfactor, bool *usemult); +static bool _bt_adjacenthtid(ItemPointer lowhtid, ItemPointer highhtid); static OffsetNumber _bt_bestsplitloc(FindSplitData *state, int perfectpenalty, bool *newitemonleft); static int _bt_strategy(FindSplitData *state, SplitPoint *leftpage, @@ -249,9 +252,10 @@ _bt_findsplitloc(Relation rel, * Start search for a split point among list of legal split points. Give * primary consideration to equalizing available free space in each half * of the split initially (start with default strategy), while applying - * rightmost where appropriate. Either of the two other fallback - * strategies may be required for cases with a large number of duplicates - * around the original/space-optimal split point. + * rightmost and split-after-new-item optimizations where appropriate. + * Either of the two other fallback strategies may be required for cases + * with a large number of duplicates around the original/space-optimal + * split point. * * Default strategy gives some weight to suffix truncation in deciding a * split point on leaf pages. It attempts to select a split point where a @@ -273,6 +277,44 @@ _bt_findsplitloc(Relation rel, usemult = true; fillfactormult = leaffillfactor / 100.0; } + else if (_bt_afternewitemoff(&state, maxoff, leaffillfactor, &usemult)) + { + /* + * New item inserted at rightmost point among a localized grouping on + * a leaf page -- apply "split after new item" optimization, either by + * applying leaf fillfactor multiplier, or by choosing the exact split + * point that leaves the new item as last on the left. (usemult is set + * for us.) + */ + if (usemult) + { + /* fillfactormult should be set based on leaf fillfactor */ + fillfactormult = leaffillfactor / 100.0; + } + else + { + /* find precise split point after newitemoff */ + for (int i = 0; i < state.nsplits; i++) + { + SplitPoint *split = state.splits + i; + + if (split->newitemonleft && + newitemoff == split->firstoldonright) + { + pfree(state.splits); + *newitemonleft = true; + return newitemoff; + } + } + + /* + * Cannot legally split after newitemoff; proceed with split + * without using fillfactor multiplier. This is defensive, and + * should never be needed in practice. + */ + fillfactormult = 0.50; + } + } else { /* Other leaf page. 50:50 page split. */ @@ -519,6 +561,192 @@ _bt_splitcmp(const void *arg1, const void *arg2) return 0; } +/* + * Subroutine to determine whether or not the page should be split immediately + * after the would-be original page offset for the new/incoming tuple. This + * is appropriate when there is a pattern of localized monotonically + * increasing insertions into a composite index, grouped by one or more + * leading attribute values. This is prevalent in many real world + * applications. Consider the example of a composite index on '(invoice_id, + * item_no)', where the item_no for each invoice is an identifier assigned in + * ascending order (invoice_id could itself be assigned in monotonically + * increasing order, but that shouldn't matter). Without this optimization, + * approximately 50% of space in leaf pages will be wasted by 50:50/!usemult + * page splits. With this optimization, space utilization will be close to + * that of a similar index where all tuple insertions modify the current + * rightmost leaf page in the index (i.e. typically 90% for leaf pages). + * + * When the optimization is applied, the new/incoming tuple becomes the last + * tuple on the new left page. (Actually, newitemoff > maxoff cases often use + * this optimization within indexes where monotonically increasing insertions + * of each grouping come in multiple "bursts" over time, such as a composite + * index on '(supplier_id, invoice_id, item_no)'. Caller applies leaf + * fillfactor in the style of a rightmost leaf page split when netitemoff is + * at or very near the end of the original page.) + * + * This optimization may leave extra free space remaining on the rightmost + * page of a "most significant column" grouping of tuples if that grouping + * never ends up having future insertions that use the free space. That + * effect is self-limiting; a future grouping that becomes the "nearest on the + * right" grouping of the affected grouping usually puts the extra free space + * to good use. In general, it's important to avoid a pattern of pathological + * page splits that consistently do the wrong thing. + * + * Caller uses optimization when routine returns true, though the exact action + * taken by caller varies. Caller uses original leaf page fillfactor in + * standard way rather than using the new item offset directly when *usemult + * was also set to true here. Otherwise, caller applies optimization by + * locating the legal split point that makes the new tuple the very last tuple + * on the left side of the split. + */ +static bool +_bt_afternewitemoff(FindSplitData *state, OffsetNumber maxoff, + int leaffillfactor, bool *usemult) +{ + int16 nkeyatts; + ItemId itemid; + IndexTuple tup; + int keepnatts; + + Assert(state->is_leaf && !state->is_rightmost); + + nkeyatts = IndexRelationGetNumberOfKeyAttributes(state->rel); + + /* Assume leaffillfactor will be used by caller for now */ + *usemult = true; + + /* Single key indexes not considered here */ + if (nkeyatts == 1) + return false; + + /* Ascending insertion pattern never inferred when new item is first */ + if (state->newitemoff == P_FIRSTKEY) + return false; + + /* + * Only apply optimization on pages with equisized tuples, since ordinal + * keys are likely to be fixed-width. Testing if the new tuple is + * variable width directly might also work, but that fails to apply the + * optimization to indexes with a numeric_ops attribute. + * + * Conclude that page has equisized tuples when the new item is the same + * width as the smallest item observed during pass over page, and other + * non-pivot tuples must be the same width as well. (Note that the + * possibly-truncated existing high key isn't counted in + * olddataitemstotal, and must be subtracted from maxoff.) + */ + if (state->newitemsz != state->minfirstrightsz) + return false; + if (state->newitemsz * (maxoff - 1) != state->olddataitemstotal) + return false; + + /* + * Avoid applying optimization when tuples are wider than a tuple + * consisting of two non-NULL int8/int64 attributes (or four non-NULL + * int4/int32 attributes) + */ + if (state->newitemsz > + MAXALIGN(sizeof(IndexTupleData) + sizeof(int64) * 2) + + sizeof(ItemIdData)) + return false; + + /* + * At least the first attribute's value must be equal to the corresponding + * value in previous tuple to apply optimization. New item cannot be a + * duplicate, either. + * + * Handle case where new item is to the right of all items on the existing + * page. This is suggestive of monotonically increasing insertions in + * itself, so the "heap TID adjacency" test is not applied here. + * Concurrent insertions from backends associated with the same grouping + * or sub-grouping should still have the optimization applied; if the + * grouping is rather large, splits will consistently end up here. + */ + if (state->newitemoff > maxoff) + { + itemid = PageGetItemId(state->page, maxoff); + tup = (IndexTuple) PageGetItem(state->page, itemid); + keepnatts = _bt_keep_natts_fast(state->rel, tup, state->newitem); + + if (keepnatts > 1 && keepnatts <= nkeyatts) + return true; + + return false; + } + + /* + * When item isn't last (or first) on page, but is deemed suitable for the + * optimization, caller splits at the point immediately after the would-be + * position of the new item, and immediately before the item after the new + * item. + * + * "Low cardinality leading column, high cardinality suffix column" + * indexes with a random insertion pattern (e.g. an index with a boolean + * column, such as an index on '(book_is_in_print, book_isbn)') present us + * with a risk of consistently misapplying the optimization. We're + * willing to accept very occasional misapplication of the optimization, + * provided the cases where we get it wrong are rare and self-limiting. + * Heap TID adjacency strongly suggests that the item just to the left was + * inserted very recently, which prevents most misfirings. Besides, all + * inappropriate cases triggered at this point will still split in the + * middle of the page on average. + */ + itemid = PageGetItemId(state->page, OffsetNumberPrev(state->newitemoff)); + tup = (IndexTuple) PageGetItem(state->page, itemid); + /* Do cheaper test first */ + if (!_bt_adjacenthtid(&tup->t_tid, &state->newitem->t_tid)) + return false; + /* Check same conditions as rightmost item case, too */ + keepnatts = _bt_keep_natts_fast(state->rel, tup, state->newitem); + + /* + * Don't allow caller to split after a new item when it will result in a + * split point to the right of the point that a leaf fillfactor split + * would use -- have caller apply leaf fillfactor instead. There is no + * advantage to being very aggressive in any case. + */ + if (keepnatts > 1 && keepnatts <= nkeyatts) + { + double interp = (double) state->newitemoff / ((double) maxoff + 1); + double leaffillfactormult = (double) leaffillfactor / 100.0; + + if (interp <= leaffillfactormult) + *usemult = false; + + return true; + } + + return false; +} + +/* + * Subroutine for determining if two heap TIDS are "adjacent". + * + * Adjacent means that the high TID is very likely to have been inserted into + * heap relation immediately after the low TID, probably by the same + * transaction. + */ +static bool +_bt_adjacenthtid(ItemPointer lowhtid, ItemPointer highhtid) +{ + BlockNumber lowblk, + highblk; + + lowblk = ItemPointerGetBlockNumber(lowhtid); + highblk = ItemPointerGetBlockNumber(highhtid); + + /* Make optimistic assumption of adjacency when heap blocks match */ + if (lowblk == highblk) + return true; + + /* When heap block one up, second offset should be FirstOffsetNumber */ + if (lowblk + 1 == highblk && + ItemPointerGetOffsetNumber(highhtid) == FirstOffsetNumber) + return true; + + return false; +} + /* * Subroutine to find the "best" split point among an array of acceptable * candidate split points that split without there being an excessively high -- 2.17.1