From b1764599e54e6b1a701b4be0c7efb1e395de17a0 Mon Sep 17 00:00:00 2001 From: Greg Burd Date: Mon, 26 Oct 2026 15:25:58 -0400 Subject: [PATCH] nbtree: opt-in leaf key prefix compression Add leaf key prefix compression to nbtree, controlled by a new per-index storage parameter, prefix_compression, that is off by default. When a leaf page is written as a whole -- during an index build, as a split half, or as the result of a deduplication pass -- and its data items share a leading byte-prefix of their first key column, the page is rebuilt in the BTP_PREFIX format: a single PREFIX item after the high key holds the shared bytes, and each data item is stored "coded" (its copy of the shared prefix replaced by a one-byte length). Reads of a leaf data item funnel through _bt_getitem(), which decodes on demand; high keys and internal-page downlinks are never coded and are read with the existing PageGetItem(). The feature is deliberately a per-page opt-in with no on-disk version change: BTREE_VERSION and the catalog version are untouched. A page is converted only when it is next written whole, never by an insert, VACUUM or key-range change, so compression "faults forward" on an existing index. Old-format and new-format pages coexist in the same index, which keeps pg_upgrade, physical replication, and standbys working with no REINDEX: a replica that has not yet seen a page's conversion still reads it correctly, and turning the parameter off (ALTER INDEX ... SET) stops new conversions while leaving existing coded pages readable. Deduplication and prefix compression compose rather than compete. A build deduplicates first and then codes the finished page, so a duplicate-heavy prefix index is no larger than a plain one; distinct keys with a shared prefix (URL or JSONB columns, the motivating case) are typically a few times smaller. Bottom-up index deletion runs on coded pages unchanged, since it only removes items. A runtime deduplication pass on a coded page decodes first, and because a coded page can hold more logical data than fits plain, the pass aborts cleanly and the page splits instead when the decoded set would overflow. WAL replay reproduces the coded bytes without resorting to full-page images in the common paths. A Relation-free layout routine, _bt_split_leaf_layout(), is shared by the primary and redo so both emit identical bytes from the same logged inputs: redo has no relcache and cannot run the prefix-choosing heuristic, so the primary logs the prefix it chose. An in-place insert logs the coded tuple; a split logs each half's chosen prefix plus the right half's plain items; a prefix deduplication logs the chosen prefix plus the deduped plain items. Each path keeps a bounded fallback to a forced image for a pathologically compressible page whose plain payload would exceed one block. amcheck decodes every leaf data item, including under heapallindexed. The create_index documentation and the nbtree README describe the parameter and the on-disk format. Tested with wal_consistency_checking=all on a primary and a streaming standby across insert, split, deduplication, delete and vacuum workloads, through crash recovery, and across a pg_upgrade that carries both page formats. --- contrib/amcheck/verify_nbtree.c | 72 +- doc/src/sgml/ref/create_index.sgml | 35 + src/backend/access/common/reloptions.c | 10 + src/backend/access/nbtree/Makefile | 1 + src/backend/access/nbtree/README | 71 ++ src/backend/access/nbtree/meson.build | 1 + src/backend/access/nbtree/nbtdedup.c | 258 ++++++- src/backend/access/nbtree/nbtinsert.c | 380 ++++++++-- src/backend/access/nbtree/nbtpage.c | 31 +- src/backend/access/nbtree/nbtprefix.c | 814 ++++++++++++++++++++++ src/backend/access/nbtree/nbtreadpage.c | 23 +- src/backend/access/nbtree/nbtree.c | 6 +- src/backend/access/nbtree/nbtsearch.c | 3 +- src/backend/access/nbtree/nbtsort.c | 54 +- src/backend/access/nbtree/nbtsplitloc.c | 84 ++- src/backend/access/nbtree/nbtutils.c | 4 +- src/backend/access/nbtree/nbtxlog.c | 309 +++++++- src/include/access/nbtree.h | 147 +++- src/include/access/nbtxlog.h | 44 +- src/test/regress/expected/btree_index.out | 256 +++++++ src/test/regress/sql/btree_index.sql | 167 +++++ src/tools/pgindent/typedefs.list | 2 + 22 files changed, 2655 insertions(+), 117 deletions(-) create mode 100644 src/backend/access/nbtree/nbtprefix.c diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c index 97c519b47f5..69aaf6bbc5d 100644 --- a/contrib/amcheck/verify_nbtree.c +++ b/contrib/amcheck/verify_nbtree.c @@ -158,7 +158,12 @@ typedef struct BtreeLastVisibleEntry OffsetNumber offset; /* Offset on index block */ int postingIndex; /* Number in the posting list (-1 for * non-deduplicated tuples) */ - ItemPointer tid; /* Heap tid */ + ItemPointer tid; /* Heap tid, or NULL; points at tiddata */ + ItemPointerData tiddata; /* Own storage for *tid: a prefix-coded leaf + * tuple is decoded into a scratch buffer that + * is reused on the next loop iteration, so we + * must keep the heap TID by value, not point + * into the (transient) decoded tuple */ } BtreeLastVisibleEntry; /* @@ -935,7 +940,8 @@ bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup, lVis->blkno = targetblock; lVis->offset = offset; lVis->postingIndex = i; - lVis->tid = tid; + ItemPointerCopy(tid, &lVis->tiddata); + lVis->tid = &lVis->tiddata; } } } @@ -961,7 +967,8 @@ bt_entry_unique_check(BtreeCheckState *state, IndexTuple itup, lVis->blkno = targetblock; lVis->offset = offset; - lVis->tid = tid; + ItemPointerCopy(tid, &lVis->tiddata); + lVis->tid = &lVis->tiddata; lVis->postingIndex = -1; } } @@ -1229,6 +1236,16 @@ bt_target_page_check(BtreeCheckState *state) OffsetNumber max; BTPageOpaque topaque; + /* + * Scratch space for decoding prefix-coded leaf tuples (_bt_getitem). + * itupbuf backs the current loop item; crossbuf backs the right sibling's + * first item in the cross-page unique check, which must coexist with the + * current item's state. A pivot, high key or non-prefix leaf item is + * returned by _bt_getitem unchanged (buffer unused). + */ + BTItemBuf itupbuf; + BTItemBuf crossbuf; + /* Last visible entry info for checking indexes with unique constraint */ BtreeLastVisibleEntry lVis = {InvalidBlockNumber, InvalidOffsetNumber, -1, NULL}; @@ -1293,8 +1310,15 @@ bt_target_page_check(BtreeCheckState *state) itemid = PageGetItemIdCareful(state, state->targetblock, state->target, offset); - itup = (IndexTuple) PageGetItem(state->target, itemid); - tupsize = IndexTupleSize(itup); + + /* + * lp_len and the tuple size limit are physical properties of the item + * as stored, so they are checked against the raw on-page tuple (coded, + * on a BTP_PREFIX leaf). Every key/content check below instead uses + * the decoded tuple returned by _bt_getitem. + */ + tupsize = IndexTupleSize((IndexTuple) PageGetItem(state->target, itemid)); + itup = _bt_getitem(state->target, offset, &itupbuf); /* * lp_len should match the IndexTuple reported length exactly, since @@ -1606,7 +1630,7 @@ bt_target_page_check(BtreeCheckState *state) itemid = PageGetItemIdCareful(state, state->targetblock, state->target, OffsetNumberNext(offset)); - itup = (IndexTuple) PageGetItem(state->target, itemid); + itup = _bt_getitem(state->target, OffsetNumberNext(offset), &itupbuf); tid = BTreeTupleGetPointsToTID(itup); nhtid = psprintf("(%u,%u)", ItemPointerGetBlockNumberNoCheck(tid), @@ -1793,7 +1817,7 @@ bt_target_page_check(BtreeCheckState *state) itemid = PageGetItemIdCareful(state, rightblock_number, rightpage, rightfirstoffset); - itup = (IndexTuple) PageGetItem(rightpage, itemid); + itup = _bt_getitem(rightpage, rightfirstoffset, &crossbuf); bt_entry_unique_check(state, itup, rightblock_number, rightfirstoffset, &lVis); @@ -1857,6 +1881,8 @@ bt_right_page_check_scankey(BtreeCheckState *state, OffsetNumber *rightfirstoffs BlockNumber targetnext; Page rightpage; OffsetNumber nline; + OffsetNumber rightoffset; + BTItemBuf firstbuf; /* Determine target's next block number */ opaque = BTPageGetOpaque(state->target); @@ -2016,6 +2042,7 @@ bt_right_page_check_scankey(BtreeCheckState *state, OffsetNumber *rightfirstoffs /* Return first data item (if any) */ rightitem = PageGetItemIdCareful(state, targetnext, rightpage, P_FIRSTDATAKEY(opaque)); + rightoffset = P_FIRSTDATAKEY(opaque); *rightfirstoffset = P_FIRSTDATAKEY(opaque); } else if (!P_ISLEAF(opaque) && @@ -2027,6 +2054,7 @@ bt_right_page_check_scankey(BtreeCheckState *state, OffsetNumber *rightfirstoffs */ rightitem = PageGetItemIdCareful(state, targetnext, rightpage, OffsetNumberNext(P_FIRSTDATAKEY(opaque))); + rightoffset = OffsetNumberNext(P_FIRSTDATAKEY(opaque)); } else { @@ -2046,9 +2074,19 @@ bt_right_page_check_scankey(BtreeCheckState *state, OffsetNumber *rightfirstoffs /* * Return first real item scankey. Note that this relies on right page * memory remaining allocated. + * + * On a BTP_PREFIX leaf the first data item is prefix-coded, so decode it; + * because the returned scankey (built by _bt_mkscankey) references the + * tuple's own storage, and _bt_getitem decodes into a buffer local to + * this function, copy the decoded tuple into the caller-reset memory + * context so it outlives the call. A pivot or non-prefix item is + * returned by _bt_getitem unchanged (it lives in the right page, which + * remains allocated), so no copy is needed then. */ - firstitup = (IndexTuple) PageGetItem(rightpage, rightitem); - return bt_mkscankey_pivotsearch(state->rel, firstitup); + firstitup = _bt_getitem(rightpage, rightoffset, &firstbuf); + if (firstitup == (IndexTuple) PageGetItem(rightpage, rightitem)) + return bt_mkscankey_pivotsearch(state->rel, firstitup); + return bt_mkscankey_pivotsearch(state->rel, CopyIndexTuple(firstitup)); } /* @@ -3095,14 +3133,13 @@ static inline bool invariant_l_offset(BtreeCheckState *state, BTScanInsert key, OffsetNumber upperbound) { - ItemId itemid; int32 cmp; Assert(!key->nextkey && key->backward); /* Verify line pointer before checking tuple */ - itemid = PageGetItemIdCareful(state, state->targetblock, state->target, - upperbound); + (void) PageGetItemIdCareful(state, state->targetblock, state->target, + upperbound); /* pg_upgrade'd indexes may legally have equal sibling tuples */ if (!key->heapkeyspace) return invariant_leq_offset(state, key, upperbound); @@ -3122,11 +3159,12 @@ invariant_l_offset(BtreeCheckState *state, BTScanInsert key, { BTPageOpaque topaque; IndexTuple ritup; + BTItemBuf ritupbuf; int uppnkeyatts; ItemPointer rheaptid; bool nonpivot; - ritup = (IndexTuple) PageGetItem(state->target, itemid); + ritup = _bt_getitem(state->target, upperbound, &ritupbuf); topaque = BTPageGetOpaque(state->target); nonpivot = P_ISLEAF(topaque) && upperbound >= P_FIRSTDATAKEY(topaque); @@ -3218,14 +3256,13 @@ invariant_l_nontarget_offset(BtreeCheckState *state, BTScanInsert key, BlockNumber nontargetblock, Page nontarget, OffsetNumber upperbound) { - ItemId itemid; int32 cmp; Assert(!key->nextkey && key->backward); /* Verify line pointer before checking tuple */ - itemid = PageGetItemIdCareful(state, nontargetblock, nontarget, - upperbound); + (void) PageGetItemIdCareful(state, nontargetblock, nontarget, + upperbound); cmp = _bt_compare(state->rel, key, nontarget, upperbound); /* pg_upgrade'd indexes may legally have equal sibling tuples */ @@ -3236,12 +3273,13 @@ invariant_l_nontarget_offset(BtreeCheckState *state, BTScanInsert key, if (cmp == 0) { IndexTuple child; + BTItemBuf childbuf; int uppnkeyatts; ItemPointer childheaptid; BTPageOpaque copaque; bool nonpivot; - child = (IndexTuple) PageGetItem(nontarget, itemid); + child = _bt_getitem(nontarget, upperbound, &childbuf); copaque = BTPageGetOpaque(nontarget); nonpivot = P_ISLEAF(copaque) && upperbound >= P_FIRSTDATAKEY(copaque); diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml index bb7505d171b..7e66765c3d3 100644 --- a/doc/src/sgml/ref/create_index.sgml +++ b/doc/src/sgml/ref/create_index.sgml @@ -477,6 +477,41 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] + + + prefix_compression (boolean) + + prefix_compression storage parameter + + + + + Controls leaf key prefix compression. When set to + ON, and the index's first key column is of a + variable-length type, a leaf page may store a shared prefix of that + column once and record only each tuple's unshared remainder, which can + substantially shrink indexes on keys that share long leading bytes + (for example URLs or jsonb keys with common paths). Set + to ON or OFF to enable or disable + the optimization. The default is OFF. + + + + + Prefix compression only changes how keys are stored; comparisons always + see the original key values, so every collation and operator class + behaves identically with it on or off. A leaf adopts a prefix only when + it is written as a whole (during an index build, or either half of a + page split), so turning prefix_compression on or off + via ALTER INDEX does not rewrite the index: existing + pages are left as they are and are converted the next time they are + written whole. No REINDEX is required, and indexes + upgraded with pg_upgrade keep working + unchanged. + + + + diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c index ea9a0417909..be1fcd9a15b 100644 --- a/src/backend/access/common/reloptions.c +++ b/src/backend/access/common/reloptions.c @@ -153,6 +153,16 @@ static relopt_bool boolRelOpts[] = }, true }, + { + { + "prefix_compression", + "Enables leaf key prefix compression for this btree index", + RELOPT_KIND_BTREE, + ShareUpdateExclusiveLock /* since it applies only to later + * whole-page writes */ + }, + false + }, /* list terminator */ {{NULL}} }; diff --git a/src/backend/access/nbtree/Makefile b/src/backend/access/nbtree/Makefile index 0daf640af96..0044d77f548 100644 --- a/src/backend/access/nbtree/Makefile +++ b/src/backend/access/nbtree/Makefile @@ -18,6 +18,7 @@ OBJS = \ nbtinsert.o \ nbtpage.o \ nbtpreprocesskeys.o \ + nbtprefix.o \ nbtreadpage.o \ nbtree.o \ nbtsearch.o \ diff --git a/src/backend/access/nbtree/README b/src/backend/access/nbtree/README index f3867547691..8aa7f9e7620 100644 --- a/src/backend/access/nbtree/README +++ b/src/backend/access/nbtree/README @@ -1081,3 +1081,74 @@ item is irrelevant, and need not be stored at all. This arrangement corresponds to the fact that an L&Y non-leaf page has one more pointer than key. Suffix truncation's negative infinity attributes behave in the same way. + +Notes about leaf key prefix compression +---------------------------------------- + +With the prefix_compression storage parameter (default off) a leaf page may +front-code the first key column of each of its data tuples against a per-page +prefix, which shrinks indexes on keys that share long leading bytes (URLs, +jsonb keys with common paths, zero-padded codes). Like deduplication, this +alters the physical representation of tuples without changing the logical +contents of the index, and comparisons always operate on the reconstructed +original values, so every collation and operator class is unaffected. + +A leaf that uses the technique carries the BTP_PREFIX page flag and one extra +item, the PREFIX item, at the offset where its first data item would otherwise +be (P_HIKEY on a rightmost page, P_FIRSTKEY otherwise); data items begin one +offset later, which P_FIRSTDATAKEY() accounts for. The PREFIX item is an +IndexTupleData header followed by 1..BTREE_PREFIX_MAX prefix bytes, shaped so +that code which walks a page's items by IndexTupleSize (such as +_bt_restore_page) steps over it. On such a page every non-pivot tuple (plain +or posting) whose first key column is not NULL stores that column as a varlena +whose payload begins with a one-byte count of how many leading bytes it shares +with the page prefix, followed by the unshared remainder (or, for a value whose +header is not the form index_form_tuple would reproduce, the whole value after +a sentinel count). Trailing key columns, INCLUDE columns, a posting list and +alignment padding follow, moved to preserve alignment; padding that is only the +key's closing zeros is dropped and restored on decode. Pivots, NULL first +columns and the high key are always stored uncompressed. Coding and decoding +depend only on the page prefix and the tuple, never the catalog, so WAL redo +reproduces them and decoding yields the original tuple byte for byte. All of +nbtprefix.c implements this; _bt_getitem() is the one accessor through which +readers obtain a decoded leaf data tuple. + +A page adopts a prefix only when it is written as a whole: a finished leaf +during an index build, or either half of a leaf page split (built coded from +the start by _bt_split_leaf_half, since a compressed leaf can hold more logical +data than fits on a plain page, choosing each half's own prefix and falling +back to a plain layout or the original page's prefix if its own does not fit). +Crucially, no insert, VACUUM, deduplication or change of a page's key range +ever rewrites a page to keep its prefix valid: a tuple that happens to share +fewer bytes simply records a smaller shared count. Because the format is a +per-page opt-in flag with no btree version change, pages without the flag are +read exactly as before, so pg_upgrade and physical/logical replication keep +working and existing indexes continue to work with no REINDEX. Turning the +parameter on with ALTER INDEX does not rewrite anything; each leaf is converted +the next time it is written whole, and compressed and uncompressed leaves +coexist in the same index indefinitely. Turning it off likewise rewrites +nothing: existing BTP_PREFIX leaves keep their coded form (the flag is cleared +only when a page is split, emptied or dedup-rewritten) and are still read back +correctly with the parameter off, since every reader decodes based on the page +flag, not on the current reloption. Scan-time workspace that must hold decoded +tuples (the index-only-scan currTuples/markTuples buffers, see +BTTupleStorageSize) is therefore sized for a decoded page on every btree index, +not just those whose parameter is currently on. + +The left half of a prefix split (block 0) is logged by full-page image +whenever the pre-split original page was prefix-compressed -- a half may end up +plain yet still be FPI-logged because its items were coded against the +original page's prefix -- and the right half (block 1) is FPI-logged whenever +it ends up prefix-compressed. A deduplication rewrite of a prefix page is +FPI-logged the same way. Full-page images are used rather than replaying +individual tuples because the coded physical layout a prefix page ends up with +would not be reconstructed by _bt_restore_page's item-by-item replay; redo +restores the image. When VACUUM or page deletion empties a prefix leaf, the +PREFIX item is removed and the flag cleared so that the invariant that an +emptied leaf holds only its high key (relied on by page deletion) still holds. + +Deduplication and bottom-up index deletion are not currently applied to +BTP_PREFIX pages (such a page splits instead): their in-place rewrite of a page +cannot always be re-compressed within one page, and prefix compression already +reclaims much of the space they target. Composing them with prefix +compression is future work. diff --git a/src/backend/access/nbtree/meson.build b/src/backend/access/nbtree/meson.build index 812f067e710..cbd34201328 100644 --- a/src/backend/access/nbtree/meson.build +++ b/src/backend/access/nbtree/meson.build @@ -6,6 +6,7 @@ backend_sources += files( 'nbtinsert.c', 'nbtpage.c', 'nbtpreprocesskeys.c', + 'nbtprefix.c', 'nbtreadpage.c', 'nbtree.c', 'nbtsearch.c', diff --git a/src/backend/access/nbtree/nbtdedup.c b/src/backend/access/nbtree/nbtdedup.c index af7affdf409..6dc7ebe25b4 100644 --- a/src/backend/access/nbtree/nbtdedup.c +++ b/src/backend/access/nbtree/nbtdedup.c @@ -69,7 +69,19 @@ _bt_dedup_pass(Relation rel, Buffer buf, IndexTuple newitem, Size newitemsz, Size pagesaving PG_USED_FOR_ASSERTS_ONLY = 0; bool singlevalstrat = false; int nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel); + bool pageprefix = P_HASPREFIX(opaque); + BTItemBuf curbuf; /* decode scratch for the loop's current item */ + BTItemBuf basebuf; /* decode scratch kept for the pending base */ XLogRecPtr recptr; + IndexTuple *dedup_plain = NULL; /* deduped plain items, for prefix WAL */ + int dedup_nplain = 0; + IndexTuple dedup_hikey = NULL; + char dedup_prefix[BTREE_PREFIX_MAX]; + Size dedup_prefixlen = 0; + bool dedup_islogprefix = false; /* page ended up prefix-coded */ + char *dedup_walbuf = NULL; /* packed prefix-dedup WAL payload */ + Size dedup_walbuflen = 0; + bool dedup_rfpi = false; /* fall back to a forced full-page image */ /* Passed-in newitemsz is MAXALIGNED but does not include line pointer */ newitemsz += sizeof(ItemIdData); @@ -120,7 +132,16 @@ _bt_dedup_pass(Relation rel, Buffer buf, IndexTuple newitem, Size newitemsz, newpage = PageGetTempPageCopySpecial(page); PageSetLSN(newpage, PageGetLSN(page)); - /* Copy high key, if any */ + /* + * The rebuilt page is laid out plain here (clear any inherited BTP_PREFIX; + * PageGetTempPageCopySpecial copies the special area but not the PREFIX + * item). If the page was prefix-compressed it is re-compressed as a whole + * below, after deduplication, honouring Strategy 1. + */ + if (pageprefix) + BTPageGetOpaque(newpage)->btpo_flags &= ~BTP_PREFIX; + + /* Copy high key, if any (a pivot, stored plain on any page) */ if (!P_RIGHTMOST(opaque)) { ItemId hitemid = PageGetItemId(page, P_HIKEY); @@ -140,12 +161,22 @@ _bt_dedup_pass(Relation rel, Buffer buf, IndexTuple newitem, Size newitemsz, Assert(!ItemIdIsDead(itemid)); + /* Decode on a prefix page so keys/posting lists are the real ones. */ + if (pageprefix) + itup = _bt_prefix_decode(page, itup, &curbuf); + if (offnum == minoff) { /* * No previous/base tuple for the data item -- use the data item - * as base tuple of pending posting list + * as base tuple of pending posting list. On a prefix page the + * base must survive the whole merge run (it is kept as a pointer + * in state and read by _bt_dedup_finish_pending), so keep it in a + * dedicated buffer rather than the per-iteration curbuf. */ + if (pageprefix) + itup = _bt_prefix_decode(page, (IndexTuple) PageGetItem(page, itemid), + &basebuf); _bt_dedup_start_pending(state, itup, offnum); } else if (state->deduplicate && @@ -170,7 +201,20 @@ _bt_dedup_pass(Relation rel, Buffer buf, IndexTuple newitem, Size newitemsz, * form new posting tuple and add it to our temp page (newpage). * Else add pending interval's base tuple to the temp page as-is. */ - pagesaving += _bt_dedup_finish_pending(newpage, state); + /* + * On a prefix page the deduped items are laid out plain here; the + * decoded set can exceed one page even when the coded form fit, so + * tolerate an overflow and abort the whole pass (the caller then + * splits the page instead of deduplicating it). + */ + { + Size saved = _bt_dedup_finish_pending(newpage, state, + pageprefix); + + if (pageprefix && saved == (Size) -1) + goto dedup_overflow; + pagesaving += saved; + } if (singlevalstrat) { @@ -198,12 +242,22 @@ _bt_dedup_pass(Relation rel, Buffer buf, IndexTuple newitem, Size newitemsz, } /* itup starts new pending posting list */ + if (pageprefix) + itup = _bt_prefix_decode(page, + (IndexTuple) PageGetItem(page, PageGetItemId(page, offnum)), + &basebuf); _bt_dedup_start_pending(state, itup, offnum); } } /* Handle the last item */ - pagesaving += _bt_dedup_finish_pending(newpage, state); + { + Size saved = _bt_dedup_finish_pending(newpage, state, pageprefix); + + if (pageprefix && saved == (Size) -1) + goto dedup_overflow; + pagesaving += saved; + } /* * If no items suitable for deduplication were found, newpage must be @@ -238,6 +292,103 @@ _bt_dedup_pass(Relation rel, Buffer buf, IndexTuple newitem, Size newitemsz, nopaque->btpo_flags &= ~BTP_HAS_GARBAGE; } + /* + * Re-apply prefix compression to the rebuilt (plain) page as a whole + * (Strategy 1), before the critical section since it allocates a scratch + * page. First capture the deduped plain items and high key: if the page + * does become prefix-compressed, redo rebuilds its coded bytes from these + * plus the chosen prefix (via _bt_split_leaf_layout), the same way a prefix + * leaf split is replayed -- no full-page image needed. + */ + if (pageprefix) + { + BTPageOpaque nopq = BTPageGetOpaque(newpage); + OffsetNumber nmin = P_FIRSTDATAKEY(nopq); + OffsetNumber nmax = PageGetMaxOffsetNumber(newpage); + + dedup_nplain = (nmax >= nmin) ? (int) nmax - (int) nmin + 1 : 0; + if (dedup_nplain > 0) + { + dedup_plain = palloc(dedup_nplain * sizeof(IndexTuple)); + for (int i = 0; i < dedup_nplain; i++) + dedup_plain[i] = CopyIndexTuple((IndexTuple) + PageGetItem(newpage, + PageGetItemId(newpage, nmin + i))); + } + if (!P_RIGHTMOST(nopq)) + { + ItemId hii = PageGetItemId(newpage, P_HIKEY); + + dedup_hikey = CopyIndexTuple((IndexTuple) PageGetItem(newpage, hii)); + } + + _bt_prefix_compress_page(rel, newpage); + + /* Did it actually compress? (It gives up if a coded item won't fit.) */ + if (P_HASPREFIX(BTPageGetOpaque(newpage))) + { + const char *pfx = _bt_page_get_prefix(newpage, &dedup_prefixlen); + + if (dedup_prefixlen > 0) + memcpy(dedup_prefix, pfx, dedup_prefixlen); + dedup_islogprefix = true; + } + } + + /* + * Pack the prefix-dedup WAL payload now, before the critical section + * (palloc is disallowed inside it): the chosen prefix bytes, then the high + * key (unless rightmost), then the deduped plain items, each MAXALIGN'd, + * in one contiguous buffer logged with a single rdata slot. + */ + if (dedup_islogprefix) + { + Size off = dedup_prefixlen; + + if (dedup_hikey != NULL) + off += MAXALIGN(IndexTupleSize(dedup_hikey)); + for (int i = 0; i < dedup_nplain; i++) + off += MAXALIGN(IndexTupleSize(dedup_plain[i])); + + /* + * A pathologically compressible page can hold more plain data than + * fits in one page; if the payload would be that large, fall back to + * a forced full-page image of the coded page (bounded at BLCKSZ). + */ + if (off > BLCKSZ) + dedup_rfpi = true; + else + { + dedup_walbuflen = off; + dedup_walbuf = palloc(Max(off, 1)); + + off = 0; + if (dedup_prefixlen > 0) + { + memcpy(dedup_walbuf, dedup_prefix, dedup_prefixlen); + off += dedup_prefixlen; + } + if (dedup_hikey != NULL) + { + Size hsz = IndexTupleSize(dedup_hikey); + + memcpy(dedup_walbuf + off, dedup_hikey, hsz); + if (MAXALIGN(hsz) != hsz) + memset(dedup_walbuf + off + hsz, 0, MAXALIGN(hsz) - hsz); + off += MAXALIGN(hsz); + } + for (int i = 0; i < dedup_nplain; i++) + { + Size sz = IndexTupleSize(dedup_plain[i]); + + memcpy(dedup_walbuf + off, dedup_plain[i], sz); + if (MAXALIGN(sz) != sz) + memset(dedup_walbuf + off + sz, 0, MAXALIGN(sz) - sz); + off += MAXALIGN(sz); + } + } + } + START_CRIT_SECTION(); PageRestoreTempPage(newpage, page); @@ -248,19 +399,38 @@ _bt_dedup_pass(Relation rel, Buffer buf, IndexTuple newitem, Size newitemsz, { xl_btree_dedup xlrec_dedup; - xlrec_dedup.nintervals = state->nintervals; + xlrec_dedup.nintervals = dedup_islogprefix ? 0 : state->nintervals; + xlrec_dedup.prefixlen = (uint8) dedup_prefixlen; + xlrec_dedup.flags = (dedup_hikey != NULL) ? XLH_DEDUP_NOT_RIGHTMOST : 0; + if (dedup_rfpi) + xlrec_dedup.flags |= XLH_DEDUP_RFPI; XLogBeginInsert(); - XLogRegisterBuffer(0, buf, REGBUF_STANDARD); + XLogRegisterBuffer(0, buf, + REGBUF_STANDARD | + (dedup_rfpi ? REGBUF_FORCE_IMAGE : 0)); XLogRegisterData(&xlrec_dedup, SizeOfBtreeDedup); - /* - * The intervals array is not in the buffer, but pretend that it is. - * When XLogInsert stores the whole buffer, the array need not be - * stored too. - */ - XLogRegisterBufData(0, state->intervals, - state->nintervals * sizeof(BTDedupInterval)); + if (dedup_islogprefix) + { + /* + * Prefix-dedup: carry the chosen prefix and the deduped plain + * items so redo can lay out the coded page; no full-page image + * (unless the payload was too large, handled by the forced image). + */ + if (!dedup_rfpi && dedup_walbuflen > 0) + XLogRegisterBufData(0, dedup_walbuf, dedup_walbuflen); + } + else + { + /* + * The intervals array is not in the buffer, but pretend that it + * is. When XLogInsert stores the whole buffer, the array need not + * be stored too. + */ + XLogRegisterBufData(0, state->intervals, + state->nintervals * sizeof(BTDedupInterval)); + } recptr = XLogInsert(RM_BTREE_ID, XLOG_BTREE_DEDUP); } @@ -277,6 +447,20 @@ _bt_dedup_pass(Relation rel, Buffer buf, IndexTuple newitem, Size newitemsz, /* cannot leak memory here */ pfree(state->htids); pfree(state); + return; + +dedup_overflow: + + /* + * A prefix page's deduped items did not fit when laid out plain. Leave + * the original page untouched and return without writing anything; the + * caller falls through to split the page. No critical section was + * entered and the buffer was not marked dirty, so the page is unchanged. + */ + Assert(pageprefix); + pfree(newpage); + pfree(state->htids); + pfree(state); } /* @@ -318,6 +502,9 @@ _bt_bottomupdel_pass(Relation rel, Buffer buf, Relation heapRel, TM_IndexDeleteOp delstate; bool neverdedup; int nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel); + bool pageprefix = P_HASPREFIX(opaque); + BTItemBuf curbuf; /* decode scratch for the loop's current item */ + BTItemBuf basebuf; /* decode scratch kept for the pending base */ /* Passed-in newitemsz is MAXALIGNED but does not include line pointer */ newitemsz += sizeof(ItemIdData); @@ -371,9 +558,24 @@ _bt_bottomupdel_pass(Relation rel, Buffer buf, Relation heapRel, Assert(!ItemIdIsDead(itemid)); + /* + * On a prefix-compressed page decode the item so _bt_keep_natts_fast + * sees the real key. The base of a pending interval must survive the + * whole run (it is compared against each following item), so keep it + * in its own buffer; the current item uses a reused buffer. Heap TIDs + * and posting lists are preserved by coding, so _bt_dedup_start_pending + * and _bt_bottomupdel_finish_pending (which read only TIDs/sizes, by + * page offset) need no decode. + */ + if (pageprefix) + itup = _bt_prefix_decode(page, itup, &curbuf); + if (offnum == minoff) { /* itup starts first pending interval */ + if (pageprefix) + itup = _bt_prefix_decode(page, (IndexTuple) PageGetItem(page, itemid), + &basebuf); _bt_dedup_start_pending(state, itup, offnum); } else if (_bt_keep_natts_fast(rel, state->base, itup) > nkeyatts && @@ -387,6 +589,9 @@ _bt_bottomupdel_pass(Relation rel, Buffer buf, Relation heapRel, _bt_bottomupdel_finish_pending(page, state, &delstate); /* itup starts new pending interval */ + if (pageprefix) + itup = _bt_prefix_decode(page, (IndexTuple) PageGetItem(page, itemid), + &basebuf); _bt_dedup_start_pending(state, itup, offnum); } } @@ -554,7 +759,7 @@ _bt_dedup_save_htid(BTDedupState state, IndexTuple itup) * where no deduplication was possible. */ Size -_bt_dedup_finish_pending(Page newpage, BTDedupState state) +_bt_dedup_finish_pending(Page newpage, BTDedupState state, bool overflowok) { OffsetNumber tupoff; Size tuplesz; @@ -572,7 +777,11 @@ _bt_dedup_finish_pending(Page newpage, BTDedupState state) Assert(tuplesz == MAXALIGN(IndexTupleSize(state->base))); Assert(tuplesz <= BTMaxItemSize); if (PageAddItem(newpage, state->base, tuplesz, tupoff, false, false) == InvalidOffsetNumber) + { + if (overflowok) + return (Size) -1; elog(ERROR, "deduplication failed to add tuple to page"); + } spacesaving = 0; } @@ -591,13 +800,26 @@ _bt_dedup_finish_pending(Page newpage, BTDedupState state) Assert(tuplesz == MAXALIGN(IndexTupleSize(final))); Assert(tuplesz <= BTMaxItemSize); if (PageAddItem(newpage, final, tuplesz, tupoff, false, false) == InvalidOffsetNumber) + { + pfree(final); + if (overflowok) + return (Size) -1; elog(ERROR, "deduplication failed to add tuple to page"); + } pfree(final); spacesaving = state->phystupsize - (tuplesz + sizeof(ItemIdData)); /* Increment nintervals, since we wrote a new posting list tuple */ state->nintervals++; - Assert(spacesaving > 0 && spacesaving < BLCKSZ); + + /* + * On an ordinary page the merged interval's tuples were all physically + * present on one page, so the saving is bounded by BLCKSZ. On a prefix + * page (overflowok) phystupsize is the DECODED size of those tuples, + * which can exceed a page (a coded page holds more logical data than + * fits plain), so only require the saving to be positive there. + */ + Assert(spacesaving > 0 && (overflowok || spacesaving < BLCKSZ)); } /* Reset state for next pending posting list */ @@ -783,16 +1005,22 @@ _bt_do_singleval(Relation rel, Page page, BTDedupState state, OffsetNumber minoff, IndexTuple newitem) { int nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel); + bool pageprefix = P_HASPREFIX(BTPageGetOpaque(page)); ItemId itemid; IndexTuple itup; + BTItemBuf buf; itemid = PageGetItemId(page, minoff); itup = (IndexTuple) PageGetItem(page, itemid); + if (pageprefix) + itup = _bt_prefix_decode(page, itup, &buf); if (_bt_keep_natts_fast(rel, newitem, itup) > nkeyatts) { itemid = PageGetItemId(page, PageGetMaxOffsetNumber(page)); itup = (IndexTuple) PageGetItem(page, itemid); + if (pageprefix) + itup = _bt_prefix_decode(page, itup, &buf); if (_bt_keep_natts_fast(rel, newitem, itup) > nkeyatts) return true; diff --git a/src/backend/access/nbtree/nbtinsert.c b/src/backend/access/nbtree/nbtinsert.c index d5d964301c1..409455ac7f2 100644 --- a/src/backend/access/nbtree/nbtinsert.c +++ b/src/backend/access/nbtree/nbtinsert.c @@ -348,8 +348,8 @@ _bt_search_insert(Relation rel, Relation heaprel, BTInsertState insertstate) P_ISLEAF(opaque) && !P_IGNORE(opaque) && PageGetFreeSpace(page) > insertstate->itemsz && - PageGetMaxOffsetNumber(page) >= P_HIKEY && - _bt_compare(rel, insertstate->itup_key, page, P_HIKEY) > 0) + PageGetMaxOffsetNumber(page) >= P_FIRSTDATAKEY(opaque) && + _bt_compare(rel, insertstate->itup_key, page, P_FIRSTDATAKEY(opaque)) > 0) { /* * Caller can use the fastpath optimization because cached @@ -1135,6 +1135,11 @@ _bt_insertonpg(Relation rel, IndexTuple oposting = NULL; IndexTuple origitup = NULL; IndexTuple nposting = NULL; + IndexTuple codeditup; /* itup coded for the page, or itup */ + Size codeditemsz; /* its MAXALIGNed size */ + IndexTuple codednposting = NULL; /* nposting coded for the page */ + BTItemBuf opostbuf; /* decode workspace for an existing posting */ + bool pageprefix; /* target page is BTP_PREFIX */ XLogRecPtr recptr; page = BufferGetPage(buf); @@ -1143,6 +1148,7 @@ _bt_insertonpg(Relation rel, isroot = P_ISROOT(opaque); isrightmost = P_RIGHTMOST(opaque); isonly = P_LEFTMOST(opaque) && P_RIGHTMOST(opaque); + pageprefix = P_HASPREFIX(opaque); /* child buffer must be given iff inserting on an internal page */ Assert(isleaf == !BufferIsValid(cbuf)); @@ -1182,6 +1188,9 @@ _bt_insertonpg(Relation rel, */ Assert(isleaf && itup_key->heapkeyspace && itup_key->allequalimage); oposting = (IndexTuple) PageGetItem(page, itemid); + /* On a prefix page the existing posting is coded; decode for the swap. */ + if (pageprefix) + oposting = _bt_prefix_decode(page, oposting, &opostbuf); /* * postingoff value comes from earlier call to _bt_binsrch_posting(). @@ -1213,19 +1222,50 @@ _bt_insertonpg(Relation rel, newitemoff = OffsetNumberNext(newitemoff); } + /* + * On a prefix-compressed page the new item is stored coded, which is at + * most MAXALIGN larger than plain and usually much smaller. Size the fit + * test and the placement by the coded size. (Split is handled on plain + * tuples; _bt_split re-compresses each half itself.) + */ + if (pageprefix) + { + codeditup = _bt_prefix_encode(page, itup); + codeditemsz = MAXALIGN(IndexTupleSize(codeditup)); + /* + * A posting-list split replaces the existing coded posting with a + * re-coded one; encode it here, before the critical section, since + * _bt_prefix_encode() allocates. + */ + if (nposting != NULL) + codednposting = _bt_prefix_encode(page, nposting); + } + else + { + codeditup = itup; + codeditemsz = itemsz; + } + /* * Do we need to split the page to fit the item on it? * * Note: PageGetFreeSpace() subtracts sizeof(ItemIdData) from its result, * so this comparison is correct even though we appear to be accounting - * only for the item and not for its line pointer. + * only for the item and not for its line pointer. The coded item size is + * used; a posting-list split additionally changes the posting tuple's + * size, but that can only shrink or keep free space (the new posting has + * one fewer TID's worth of room reused by newitem), so the item fit test + * remains a safe upper bound. */ - if (PageGetFreeSpace(page) < itemsz) + if (PageGetFreeSpace(page) < codeditemsz) { Buffer rbuf; Assert(!split_only_page); + if (codeditup != itup) + pfree(codeditup); + /* split the buffer into left and right halves */ rbuf = _bt_split(rel, heaprel, itup_key, buf, cbuf, newitemoff, itemsz, itup, origitup, nposting, postingoff); @@ -1295,9 +1335,25 @@ _bt_insertonpg(Relation rel, START_CRIT_SECTION(); if (postingoff != 0) - memcpy(oposting, nposting, MAXALIGN(IndexTupleSize(nposting))); + { + if (pageprefix) + { + /* + * The existing posting is stored coded; replace its slot (at + * the offset just before newitemoff, since newitemoff was + * advanced past it above) with the coded new posting (encoded + * before the critical section), whose size may differ. + */ + if (!PageIndexTupleOverwrite(page, OffsetNumberPrev(newitemoff), + codednposting, IndexTupleSize(codednposting))) + elog(PANIC, "failed to overwrite posting list in block %u in index \"%s\"", + BufferGetBlockNumber(buf), RelationGetRelationName(rel)); + } + else + memcpy(oposting, nposting, MAXALIGN(IndexTupleSize(nposting))); + } - if (PageAddItem(page, itup, itemsz, newitemoff, false, false) == InvalidOffsetNumber) + if (PageAddItem(page, codeditup, codeditemsz, newitemoff, false, false) == InvalidOffsetNumber) elog(PANIC, "failed to add new item to block %u in index \"%s\"", BufferGetBlockNumber(buf), RelationGetRelationName(rel)); @@ -1381,8 +1437,30 @@ _bt_insertonpg(Relation rel, } } - XLogRegisterBuffer(0, buf, REGBUF_STANDARD); - if (postingoff == 0) + XLogRegisterBuffer(0, buf, + REGBUF_STANDARD | + ((pageprefix && postingoff != 0) ? REGBUF_FORCE_IMAGE : 0)); + if (pageprefix && postingoff != 0) + { + /* + * A posting-list split on a prefix page rewrites an existing + * coded posting and adds a coded item; rather than teach redo to + * re-code both, force a full-page image (this is a rare case). + * postingoff is still recorded for the record format. + */ + upostingoff = postingoff; + XLogRegisterBufData(0, &upostingoff, sizeof(uint16)); + } + else if (pageprefix) + { + /* + * Simple insert on a prefix page: log the CODED item bytes as + * they were placed on the page, so redo adds them verbatim at + * newitemoff -- no full-page image needed, and byte-identical. + */ + XLogRegisterBufData(0, (char *) codeditup, IndexTupleSize(codeditup)); + } + else if (postingoff == 0) { /* Just log itup from caller */ XLogRegisterBufData(0, itup, IndexTupleSize(itup)); @@ -1418,6 +1496,11 @@ _bt_insertonpg(Relation rel, END_CRIT_SECTION(); + if (codeditup != itup) + pfree(codeditup); + if (codednposting != NULL && codednposting != nposting) + pfree(codednposting); + /* Release subsidiary buffers */ if (BufferIsValid(metabuf)) _bt_relbuf(rel, metabuf); @@ -1506,6 +1589,21 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, ItemId itemid; IndexTuple firstright, lefthighkey; + BTItemBuf frbuf; /* decode scratch for firstright */ + BTItemBuf llbuf; /* decode scratch for lastleft */ + IndexTuple *leftitems = NULL; /* leaf: plain items for the left half */ + IndexTuple *rightitems = NULL; /* leaf: plain items for the right half */ + int nleft = 0; + int nright = 0; + IndexTuple righthighkey = NULL; /* leaf right half's high key (NULL=rightmost) */ + const char *origprefix = NULL; + Size origprefixlen = 0; + char leftprefix[BTREE_PREFIX_MAX]; /* chosen prefix of each leaf half */ + char rightprefix[BTREE_PREFIX_MAX]; + Size leftprefixlen = 0; + Size rightprefixlen = 0; + char *rightwalbuf = NULL; /* packed right-page WAL payload */ + Size rightwalbuflen = 0; OffsetNumber firstrightoff; OffsetNumber afterleftoff, afterrightoff, @@ -1575,7 +1673,7 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, * and HAS_GARBAGE flags. */ lopaque->btpo_flags = oopaque->btpo_flags; - lopaque->btpo_flags &= ~(BTP_ROOT | BTP_SPLIT_END | BTP_HAS_GARBAGE); + lopaque->btpo_flags &= ~(BTP_ROOT | BTP_SPLIT_END | BTP_HAS_GARBAGE | BTP_PREFIX); /* set flag in leftpage indicating that rightpage has no downlink yet */ lopaque->btpo_flags |= BTP_INCOMPLETE_SPLIT; lopaque->btpo_prev = oopaque->btpo_prev; @@ -1650,6 +1748,8 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, itemid = PageGetItemId(origpage, firstrightoff); itemsz = ItemIdGetLength(itemid); firstright = (IndexTuple) PageGetItem(origpage, itemid); + if (P_HASPREFIX(oopaque)) + firstright = _bt_prefix_decode(origpage, firstright, &frbuf); if (firstrightoff == origpagepostingoff) firstright = nposting; } @@ -1673,6 +1773,8 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, Assert(lastleftoff >= P_FIRSTDATAKEY(oopaque)); itemid = PageGetItemId(origpage, lastleftoff); lastleft = (IndexTuple) PageGetItem(origpage, itemid); + if (P_HASPREFIX(oopaque)) + lastleft = _bt_prefix_decode(origpage, lastleft, &llbuf); if (lastleftoff == origpagepostingoff) lastleft = nposting; } @@ -1712,7 +1814,11 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, } /* - * Add new high key to leftpage + * Add new high key to leftpage. For a leaf split the left half is built + * as a whole below (so it can be prefix-compressed from the start, which + * a plain incremental build cannot represent when the page holds more + * logical data than fits plain); the high key is passed to the builder + * then. Internal halves are built incrementally here as before. */ afterleftoff = P_HIKEY; @@ -1720,11 +1826,23 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, Assert(BTreeTupleGetNAtts(lefthighkey, rel) <= IndexRelationGetNumberOfKeyAttributes(rel)); Assert(itemsz == MAXALIGN(IndexTupleSize(lefthighkey))); - if (PageAddItem(leftpage, lefthighkey, itemsz, afterleftoff, false, false) == InvalidOffsetNumber) - elog(ERROR, "failed to add high key to the left sibling" - " while splitting block %u of index \"%s\"", - origpagenumber, RelationGetRelationName(rel)); - afterleftoff = OffsetNumberNext(afterleftoff); + if (isleaf) + { + /* The builder needs a stable copy; lefthighkey points into workspace. */ + lefthighkey = CopyIndexTuple(lefthighkey); + leftitems = palloc_array(IndexTuple, maxoff + 2); + rightitems = palloc_array(IndexTuple, maxoff + 2); + if (P_HASPREFIX(oopaque)) + origprefix = _bt_page_get_prefix(origpage, &origprefixlen); + } + else + { + if (PageAddItem(leftpage, lefthighkey, itemsz, afterleftoff, false, false) == InvalidOffsetNumber) + elog(ERROR, "failed to add high key to the left sibling" + " while splitting block %u of index \"%s\"", + origpagenumber, RelationGetRelationName(rel)); + afterleftoff = OffsetNumberNext(afterleftoff); + } /* * Acquire a new right page to split into, now that left page has a new @@ -1765,7 +1883,7 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, * and HAS_GARBAGE flags. */ ropaque->btpo_flags = oopaque->btpo_flags; - ropaque->btpo_flags &= ~(BTP_ROOT | BTP_SPLIT_END | BTP_HAS_GARBAGE); + ropaque->btpo_flags &= ~(BTP_ROOT | BTP_SPLIT_END | BTP_HAS_GARBAGE | BTP_PREFIX); ropaque->btpo_prev = origpagenumber; ropaque->btpo_next = oopaque->btpo_next; ropaque->btpo_level = oopaque->btpo_level; @@ -1782,21 +1900,25 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, if (!isrightmost) { - IndexTuple righthighkey; + ItemId hiitemid = PageGetItemId(origpage, P_HIKEY); + Size hiitemsz = ItemIdGetLength(hiitemid); + IndexTuple hikey = (IndexTuple) PageGetItem(origpage, hiitemid); - itemid = PageGetItemId(origpage, P_HIKEY); - itemsz = ItemIdGetLength(itemid); - righthighkey = (IndexTuple) PageGetItem(origpage, itemid); - Assert(BTreeTupleGetNAtts(righthighkey, rel) > 0); - Assert(BTreeTupleGetNAtts(righthighkey, rel) <= + Assert(BTreeTupleGetNAtts(hikey, rel) > 0); + Assert(BTreeTupleGetNAtts(hikey, rel) <= IndexRelationGetNumberOfKeyAttributes(rel)); - if (PageAddItem(rightpage, righthighkey, itemsz, afterrightoff, false, false) == InvalidOffsetNumber) + if (isleaf) + righthighkey = CopyIndexTuple(hikey); /* stable: origpage is + * overwritten by the left + * half before WAL logging */ + else { - elog(ERROR, "failed to add high key to the right sibling" - " while splitting block %u of index \"%s\"", - origpagenumber, RelationGetRelationName(rel)); + if (PageAddItem(rightpage, hikey, hiitemsz, afterrightoff, false, false) == InvalidOffsetNumber) + elog(ERROR, "failed to add high key to the right sibling" + " while splitting block %u of index \"%s\"", + origpagenumber, RelationGetRelationName(rel)); + afterrightoff = OffsetNumberNext(afterrightoff); } - afterrightoff = OffsetNumberNext(afterrightoff); } /* @@ -1817,11 +1939,23 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, for (i = P_FIRSTDATAKEY(oopaque); i <= maxoff; i = OffsetNumberNext(i)) { IndexTuple dataitem; + BTItemBuf dibuf; itemid = PageGetItemId(origpage, i); itemsz = ItemIdGetLength(itemid); dataitem = (IndexTuple) PageGetItem(origpage, itemid); + /* + * On a prefix-compressed origpage decode each data item to its plain + * form before placing it; the halves are built plain here and each is + * re-compressed as a whole below (Strategy 1). itemsz follows. + */ + if (isleaf && P_HASPREFIX(oopaque)) + { + dataitem = _bt_prefix_decode(origpage, dataitem, &dibuf); + itemsz = MAXALIGN(IndexTupleSize(dataitem)); + } + /* replace original item with nposting due to posting split? */ if (i == origpagepostingoff) { @@ -1836,25 +1970,25 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, if (newitemonleft) { Assert(newitemoff <= firstrightoff); - if (!_bt_pgaddtup(leftpage, newitemsz, newitem, afterleftoff, - false)) - { + if (isleaf) + leftitems[nleft++] = newitem; + else if (!_bt_pgaddtup(leftpage, newitemsz, newitem, afterleftoff, + false)) elog(ERROR, "failed to add new item to the left sibling" " while splitting block %u of index \"%s\"", origpagenumber, RelationGetRelationName(rel)); - } afterleftoff = OffsetNumberNext(afterleftoff); } else { Assert(newitemoff >= firstrightoff); - if (!_bt_pgaddtup(rightpage, newitemsz, newitem, afterrightoff, - afterrightoff == minusinfoff)) - { + if (isleaf) + rightitems[nright++] = newitem; + else if (!_bt_pgaddtup(rightpage, newitemsz, newitem, afterrightoff, + afterrightoff == minusinfoff)) elog(ERROR, "failed to add new item to the right sibling" " while splitting block %u of index \"%s\"", origpagenumber, RelationGetRelationName(rel)); - } afterrightoff = OffsetNumberNext(afterrightoff); } } @@ -1862,23 +1996,23 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, /* decide which page to put it on */ if (i < firstrightoff) { - if (!_bt_pgaddtup(leftpage, itemsz, dataitem, afterleftoff, false)) - { + if (isleaf) + leftitems[nleft++] = CopyIndexTuple(dataitem); + else if (!_bt_pgaddtup(leftpage, itemsz, dataitem, afterleftoff, false)) elog(ERROR, "failed to add old item to the left sibling" " while splitting block %u of index \"%s\"", origpagenumber, RelationGetRelationName(rel)); - } afterleftoff = OffsetNumberNext(afterleftoff); } else { - if (!_bt_pgaddtup(rightpage, itemsz, dataitem, afterrightoff, - afterrightoff == minusinfoff)) - { + if (isleaf) + rightitems[nright++] = CopyIndexTuple(dataitem); + else if (!_bt_pgaddtup(rightpage, itemsz, dataitem, afterrightoff, + afterrightoff == minusinfoff)) elog(ERROR, "failed to add old item to the right sibling" " while splitting block %u of index \"%s\"", origpagenumber, RelationGetRelationName(rel)); - } afterrightoff = OffsetNumberNext(afterrightoff); } } @@ -1892,16 +2026,85 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, * not be splitting the page). */ Assert(!newitemonleft && newitemoff == maxoff + 1); - if (!_bt_pgaddtup(rightpage, newitemsz, newitem, afterrightoff, - afterrightoff == minusinfoff)) - { + if (isleaf) + rightitems[nright++] = newitem; + else if (!_bt_pgaddtup(rightpage, newitemsz, newitem, afterrightoff, + afterrightoff == minusinfoff)) elog(ERROR, "failed to add new item to the right sibling" " while splitting block %u of index \"%s\"", origpagenumber, RelationGetRelationName(rel)); - } afterrightoff = OffsetNumberNext(afterrightoff); } + /* + * Leaf halves are built as whole pages now, so they can be prefix- + * compressed from the start (Strategy 1: a page is faulted into the + * BTP_PREFIX format only when it is written whole). Both halves of a + * prefix split are restored from full-page images in redo (their coded + * physical layout would not be reproduced by item replay): the left half + * (block 0) is FPI-logged whenever the original page was prefix-compressed + * and redo detects the image via BLK_RESTORED, so it needs no record flag; + * the right half (block 1) records XLH_SPLIT_RPREFIX so redo restores it + * from its forced image rather than rebuilding it from logged tuples. + * dataitem copies and the lefthighkey copy are freed via the short-lived + * insert context. + */ + if (isleaf) + { + _bt_split_leaf_half(rel, leftpage, lefthighkey, leftitems, nleft, + origprefix, origprefixlen, + leftprefix, &leftprefixlen); + _bt_split_leaf_half(rel, rightpage, righthighkey, rightitems, nright, + origprefix, origprefixlen, + rightprefix, &rightprefixlen); + + /* + * Build the right page's WAL payload now, before the critical section + * (palloc is disallowed inside it): the right prefix bytes, then the + * right high key (unless rightmost), then the right half's plain items, + * each MAXALIGN'd, in one contiguous buffer logged with a single rdata + * slot. A very compressible right half whose plain items would exceed + * a page is instead carried as a full-page image (set below), so this + * buffer is bounded. + */ + if (leftprefixlen > 0 || rightprefixlen > 0 || origprefixlen > 0) + { + Size roff = rightprefixlen; + + if (righthighkey != NULL) + roff += MAXALIGN(IndexTupleSize(righthighkey)); + for (int ri = 0; ri < nright; ri++) + roff += MAXALIGN(IndexTupleSize(rightitems[ri])); + rightwalbuf = palloc(Max(roff, 1)); + rightwalbuflen = roff; + + roff = 0; + if (rightprefixlen > 0) + { + memcpy(rightwalbuf, rightprefix, rightprefixlen); + roff += rightprefixlen; + } + if (righthighkey != NULL) + { + Size hsz = IndexTupleSize(righthighkey); + + memcpy(rightwalbuf + roff, righthighkey, hsz); + if (MAXALIGN(hsz) != hsz) + memset(rightwalbuf + roff + hsz, 0, MAXALIGN(hsz) - hsz); + roff += MAXALIGN(hsz); + } + for (int ri = 0; ri < nright; ri++) + { + Size rsz = IndexTupleSize(rightitems[ri]); + + memcpy(rightwalbuf + roff, rightitems[ri], rsz); + if (MAXALIGN(rsz) != rsz) + memset(rightwalbuf + roff + rsz, 0, MAXALIGN(rsz) - rsz); + roff += MAXALIGN(rsz); + } + } + } + /* * We have to grab the original right sibling (if any) and update its prev * link. We are guaranteed that this is deadlock-free, since we couple @@ -2005,17 +2208,71 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, if (postingoff != 0 && origpagepostingoff < firstrightoff) xlrec.postingoff = postingoff; + /* + * A leaf split that involves prefix compression (either half ends up + * compressed, or the pre-split page was compressed so its items are + * coded) is replayed by rebuilding both halves from the pre-split page, + * exactly as the primary did, rather than by replaying logged tuples -- + * the coded physical layout would not match _bt_restore_page. No + * full-page image is needed; redo reconstructs the plain items from the + * original page (block 0) and lays each half out coded against the + * chosen prefix, whose length and bytes are carried in the record. + */ + xlrec.flags = 0; + xlrec.leftprefixlen = 0; + xlrec.rightprefixlen = 0; + if (isleaf && (leftprefixlen > 0 || rightprefixlen > 0 || origprefixlen > 0)) + { + xlrec.flags |= XLH_SPLIT_LEAFPREFIX; + xlrec.leftprefixlen = (uint8) leftprefixlen; + xlrec.rightprefixlen = (uint8) rightprefixlen; + + /* + * The right half is normally carried as its packed plain payload + * (redo lays it out coded), far cheaper than a full-page image. + * But a very compressible page can hold more plain data than fits + * in one page; if the payload would be that large, fall back to a + * forced full-page image of the coded right page (bounded at BLCKSZ). + */ + if (rightwalbuflen > BLCKSZ) + xlrec.flags |= XLH_SPLIT_RFPI; + } + XLogBeginInsert(); XLogRegisterData(&xlrec, SizeOfBtreeSplit); XLogRegisterBuffer(0, buf, REGBUF_STANDARD); - XLogRegisterBuffer(1, rbuf, REGBUF_WILL_INIT); + /* + * The right page is rebuilt from scratch in redo. Ordinarily that is + * from the logged tuple region (REGBUF_WILL_INIT). A prefix-leaf split + * instead rebuilds it from its logged plain items, unless those would + * overflow the record (XLH_SPLIT_RFPI), in which case the coded page is + * carried as a forced full-page image. + */ + XLogRegisterBuffer(1, rbuf, + (xlrec.flags & XLH_SPLIT_RFPI) ? + (REGBUF_STANDARD | REGBUF_FORCE_IMAGE) : REGBUF_WILL_INIT); /* Log original right sibling, since we've changed its prev-pointer */ if (!isrightmost) XLogRegisterBuffer(2, sbuf, REGBUF_STANDARD); if (!isleaf) XLogRegisterBuffer(3, cbuf, REGBUF_STANDARD); + /* + * For a prefix leaf split, carry each half's chosen prefix bytes, and + * (unless the right page is carried as a full-page image) the right + * high key and the right half's PLAIN items on block 1, so redo can lay + * the right page out coded without depending on block 0's state (block + * 0 may be restored from an automatic full-page image). The left half + * is rebuilt in redo from the original page (block 0) when it needs + * redo, or from its own FPI otherwise, so the left items are not logged. + */ + if (xlrec.flags & XLH_SPLIT_LEAFPREFIX) + { + if (!(xlrec.flags & XLH_SPLIT_RFPI) && rightwalbuflen > 0) + XLogRegisterBufData(1, rightwalbuf, rightwalbuflen); + } + /* * Log the new item, if it was inserted on the left page. (If it was * put on the right page, we don't need to explicitly WAL log it @@ -2063,6 +2320,14 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, XLogRegisterBufData(0, lefthighkey, MAXALIGN(IndexTupleSize(lefthighkey))); + /* + * For a prefix leaf split, the left half's chosen prefix bytes follow + * the high key on block 0 (redo reads newitem/orignewitem, then the + * high key, then this prefix). + */ + if ((xlrec.flags & XLH_SPLIT_LEAFPREFIX) && leftprefixlen > 0) + XLogRegisterBufData(0, leftprefix, leftprefixlen); + /* * Log the contents of the right page in the format understood by * _bt_restore_page(). The whole right page will be recreated. @@ -2071,11 +2336,14 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, * some new func in page API. Note we only store the tuples * themselves, knowing that they were inserted in item-number order * and so the line pointers can be reconstructed. See comments for - * _bt_restore_page(). + * _bt_restore_page(). A prefix-compressed leaf split instead rebuilds + * the right page from the original page in redo (see XLH_SPLIT_LEAFPREFIX), + * so its tuple region is not logged. */ - XLogRegisterBufData(1, - (char *) rightpage + ((PageHeader) rightpage)->pd_upper, - ((PageHeader) rightpage)->pd_special - ((PageHeader) rightpage)->pd_upper); + if (!(xlrec.flags & XLH_SPLIT_LEAFPREFIX)) + XLogRegisterBufData(1, + (char *) rightpage + ((PageHeader) rightpage)->pd_upper, + ((PageHeader) rightpage)->pd_special - ((PageHeader) rightpage)->pd_upper); xlinfo = newitemonleft ? XLOG_BTREE_SPLIT_L : XLOG_BTREE_SPLIT_R; recptr = XLogInsert(RM_BTREE_ID, xlinfo); @@ -2820,7 +3088,15 @@ _bt_delete_or_dedup_one_page(Relation rel, Relation heapRel, _bt_bottomupdel_pass(rel, buffer, heapRel, insertstate->itemsz)) return; - /* Perform deduplication pass (when enabled and index-is-allequalimage) */ + /* + * Perform deduplication pass (when enabled and index-is-allequalimage). + * + * On a prefix-compressed page the pass decodes items and lays the deduped + * result out plain before re-coding it; the decoded set can exceed one + * page even when the coded form fit, in which case the pass aborts without + * touching the page and we fall through to split it. When it does fit, the + * recompressed page's WAL replays without a full-page image. + */ if (BTGetDeduplicateItems(rel) && itup_key->allequalimage) _bt_dedup_pass(rel, buffer, insertstate->itup, insertstate->itemsz, (indexUnchanged || uniquedup)); diff --git a/src/backend/access/nbtree/nbtpage.c b/src/backend/access/nbtree/nbtpage.c index 410514d8e10..706177f7992 100644 --- a/src/backend/access/nbtree/nbtpage.c +++ b/src/backend/access/nbtree/nbtpage.c @@ -1212,11 +1212,26 @@ _bt_delitems_vacuum(Relation rel, Buffer buf, if (ndeletable > 0) PageIndexMultiDelete(page, deletable, ndeletable); + opaque = BTPageGetOpaque(page); + + /* + * If VACUUM emptied a prefix-compressed leaf of all its data items, drop + * the PREFIX item too and clear BTP_PREFIX: an empty page carries no + * prefix, and page deletion relies on an emptied leaf holding only its + * high key (nbtpage.c's _bt_mark_page_halfdead). Recorded in the vacuum + * record so redo does the same. + */ + if (P_HASPREFIX(opaque) && + PageGetMaxOffsetNumber(page) == BTPageGetPrefixOff(opaque)) + { + PageIndexTupleDelete(page, BTPageGetPrefixOff(opaque)); + opaque->btpo_flags &= ~BTP_PREFIX; + } + /* * We can clear the vacuum cycle ID since this page has certainly been * processed by the current vacuum scan. */ - opaque = BTPageGetOpaque(page); opaque->btpo_cycleid = 0; /* @@ -2245,6 +2260,20 @@ _bt_mark_page_halfdead(Relation rel, Relation heaprel, Buffer leafbuf, opaque = BTPageGetOpaque(page); opaque->btpo_flags |= BTP_HALF_DEAD; + /* + * A prefix-compressed leaf being deleted may still carry its PREFIX item + * (e.g. emptied by simple deletion, which does not drop it). Remove it + * and clear BTP_PREFIX so the page holds only its high key, as the + * half-dead transition below requires. Redo (btree_xlog_mark_page_halfdead) + * reinitializes the page, so it needs no separate handling. + */ + if (P_HASPREFIX(opaque) && + PageGetMaxOffsetNumber(page) == BTPageGetPrefixOff(opaque)) + { + PageIndexTupleDelete(page, BTPageGetPrefixOff(opaque)); + opaque->btpo_flags &= ~BTP_PREFIX; + } + Assert(PageGetMaxOffsetNumber(page) == P_HIKEY); MemSet(&trunctuple, 0, sizeof(IndexTupleData)); trunctuple.t_info = sizeof(IndexTupleData); diff --git a/src/backend/access/nbtree/nbtprefix.c b/src/backend/access/nbtree/nbtprefix.c new file mode 100644 index 00000000000..c725f75cd92 --- /dev/null +++ b/src/backend/access/nbtree/nbtprefix.c @@ -0,0 +1,814 @@ +/*------------------------------------------------------------------------- + * + * nbtprefix.c + * Leaf key prefix compression for Postgres btrees. + * + * A leaf page of an index whose prefix_compression reloption is on, and whose + * first key column is of a varlena type, may be flagged BTP_PREFIX. Such a + * page front-codes the first key column of each non-pivot tuple against a + * per-page PREFIX item, so that a long key that repeats on a page costs + * little more than its unshared tail. The on-disk format is documented at + * BTP_PREFIX in nbtree.h; this file is the encoder, decoder, and the policy + * that chooses a page's prefix. + * + * Coding and decoding depend only on the page's prefix and the tuple, not on + * the catalog, so WAL redo repeats them, and decoding reproduces the plain + * tuple byte for byte. A tuple inserted later that shares less of the prefix + * just stores a smaller `shared' count, so no insert, VACUUM, deduplication + * or change of the page's key range ever has to rewrite the page to keep the + * prefix valid. The prefix is chosen only when a page is written as a whole + * (CREATE INDEX, or a half of a split). + * + * This is modeled on the leaf prefix compression of the BARK access method + * (src/backend/access/bark/barkutils.c). + * + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/access/nbtree/nbtprefix.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/itup.h" +#include "access/nbtree.h" +#include "storage/bufpage.h" +#include "utils/rel.h" + +/* + * Where each part of a tuple goes in its coded form. Coding and decoding + * both lay the tuple out from this, so the two cannot disagree. + */ +typedef struct BTPrefixPlan +{ + Size hoff; /* data offset: header and null bitmap */ + Size c1; /* plain first column's size, with header */ + Size keyend; /* end of the plain key (posting offset) */ + uint8 code; /* first payload byte of the coded column */ + const char *suffix; /* bytes stored after it */ + Size suffixlen; + Size vsize; /* coded column's size, with header */ + Size codedkeyend; /* end of the coded key */ + Size total; /* size of the coded tuple */ +} BTPrefixPlan; + +/* + * Is `itup' stored coded on a BTP_PREFIX page? Only a non-pivot tuple (plain + * or posting) whose first column is not NULL; a pivot (high key, downlink) + * is stored as it is. + */ +static bool +_bt_prefix_codable(IndexTuple itup) +{ + if (BTreeTupleIsPivot(itup)) + return false; + return !IndexTupleHasNulls(itup) || + !att_isnull(0, (uint8 *) itup + sizeof(IndexTupleData)); +} + +/* + * Does the varlena `val' have the header decoding would give it back: the + * 1-byte header if its data fit one, else an uncompressed 4-byte header? + */ +static bool +_bt_prefix_canonical(const char *val) +{ + if (VARATT_IS_SHORT(val)) + return !VARATT_IS_EXTERNAL(val); + return !VARATT_IS_EXTENDED(val) && + VARSIZE(val) - VARHDRSZ + VARHDRSZ_SHORT > VARATT_SHORT_MAX; +} + +/* + * End of a leaf tuple's key: the posting-list offset for a posting tuple, the + * whole tuple size otherwise. The bytes after the key (a posting list) are + * moved as they are. + */ +static Size +_bt_prefix_keyend(IndexTuple itup) +{ + if (BTreeTupleIsPosting(itup)) + return BTreeTupleGetPostingOffset(itup); + return IndexTupleSize(itup); +} + +/* + * The size of a coded first column whose payload is `payload' bytes: a + * varlena with a 1-byte header when it fits one. + */ +static inline Size +_bt_prefix_vsize(Size payload) +{ + if (payload + VARHDRSZ_SHORT <= VARATT_SHORT_MAX) + return payload + VARHDRSZ_SHORT; + return payload + VARHDRSZ; +} + +/* + * Offset at which the bytes after the first column go in a coded tuple whose + * coded column ends at `vend', when in the plain tuple they start at + * `restorig': the first offset at or after `vend' that is the same distance + * past a MAXALIGN boundary. + */ +static inline Size +_bt_prefix_restoff(Size vend, Size restorig) +{ + return vend + (MAXIMUM_ALIGNOF + restorig % MAXIMUM_ALIGNOF - + vend % MAXIMUM_ALIGNOF) % MAXIMUM_ALIGNOF; +} + +/* Plan the coding of the codable tuple `itup' against `prefix'. */ +static void +_bt_prefix_plan(const char *prefix, Size prefixlen, IndexTuple itup, + BTPrefixPlan *plan) +{ + const char *val; + Size restorig; + bool storerest; + + plan->hoff = IndexInfoFindDataOffset(itup->t_info); + val = (const char *) itup + plan->hoff; + plan->c1 = VARSIZE_ANY(val); + plan->keyend = _bt_prefix_keyend(itup); + + if (_bt_prefix_canonical(val)) + { + const char *data = VARDATA_ANY(val); + Size datalen = VARSIZE_ANY_EXHDR(val); + Size limit = Min(prefixlen, datalen); + Size shared = 0; + + while (shared < limit && prefix[shared] == data[shared]) + shared++; + plan->code = (uint8) shared; + plan->suffix = data + shared; + plan->suffixlen = datalen - shared; + } + else + { + plan->code = BTREE_PREFIX_RAW; + plan->suffix = val; + plan->suffixlen = plan->c1; + } + plan->vsize = _bt_prefix_vsize(1 + plan->suffixlen); + + /* + * The bytes after the first column can be left out when they are just the + * zero padding that ends the key. + */ + restorig = plan->hoff + plan->c1; + storerest = plan->keyend != MAXALIGN(restorig); + for (Size i = restorig; !storerest && i < plan->keyend; i++) + storerest = ((const char *) itup)[i] != 0; + + if (storerest) + { + plan->code |= BTREE_PREFIX_REST; + plan->codedkeyend = _bt_prefix_restoff(plan->hoff + plan->vsize, + restorig) + + (plan->keyend - restorig); + } + else + plan->codedkeyend = MAXALIGN(plan->hoff + plan->vsize); + plan->total = plan->codedkeyend + (IndexTupleSize(itup) - plan->keyend); +} + +/* + * Copy n bytes, as memcpy. Decoding moves a few short runs of bytes per + * tuple; eight bytes at a time is several times faster than a libc memcpy + * for runs this short (the string-move startup cost dominates otherwise). + */ +static inline void +_bt_prefix_copy(char *dst, const char *src, Size n) +{ + while (n >= sizeof(uint64)) + { + memcpy(dst, src, sizeof(uint64)); + dst += sizeof(uint64); + src += sizeof(uint64); + n -= sizeof(uint64); + } + while (n-- > 0) + *dst++ = *src++; +} + +/* The prefix of a BTP_PREFIX page, and its length in *len. */ +const char * +_bt_page_get_prefix(Page page, Size *len) +{ + BTPageOpaque opaque = BTPageGetOpaque(page); + IndexTuple item; + + Assert(P_HASPREFIX(opaque)); + item = (IndexTuple) PageGetItem(page, + PageGetItemId(page, + BTPageGetPrefixOff(opaque))); + *len = IndexTupleSize(item) - sizeof(IndexTupleData); + return (const char *) item + sizeof(IndexTupleData); +} + +/* + * Give `page', a leaf holding nothing yet but its high key (if it has one), + * the prefix `prefix' of `len' bytes: add the PREFIX item and set BTP_PREFIX. + * Its padding comes from the page's free space, which _bt_pageinit zeroed, so + * a page rebuilt in redo is the same. + */ +void +_bt_page_set_prefix(Page page, const char *prefix, Size len) +{ + BTPageOpaque opaque = BTPageGetOpaque(page); + union + { + IndexTupleData hdr; + char data[sizeof(IndexTupleData) + BTREE_PREFIX_MAX]; + } item; + + Assert(P_ISLEAF(opaque) && !P_HASPREFIX(opaque)); + Assert(len >= 1 && len <= BTREE_PREFIX_MAX); + Assert(PageGetMaxOffsetNumber(page) == BTPageGetPrefixOff(opaque) - 1); + + memset(&item.hdr, 0, sizeof(IndexTupleData)); + item.hdr.t_info = (unsigned short) (sizeof(IndexTupleData) + len); + memcpy(item.data + sizeof(IndexTupleData), prefix, len); + if (PageAddItem(page, item.data, sizeof(IndexTupleData) + len, + BTPageGetPrefixOff(opaque), false, false) == + InvalidOffsetNumber) + elog(ERROR, "failed to add prefix item to btree page"); + opaque->btpo_flags |= BTP_PREFIX; +} + +/* + * The coded form of the leaf tuple `itup' for `page', palloc'd; `itup' itself + * when the page has no prefix or the tuple is stored plain. + */ +IndexTuple +_bt_prefix_encode(Page page, IndexTuple itup) +{ + const char *prefix; + Size prefixlen; + BTPrefixPlan plan; + char *coded; + char *v; + char *payload; + + if (!P_HASPREFIX(BTPageGetOpaque(page)) || + !_bt_prefix_codable(itup)) + return itup; + + prefix = _bt_page_get_prefix(page, &prefixlen); + _bt_prefix_plan(prefix, prefixlen, itup, &plan); + Assert(plan.total <= INDEX_SIZE_MASK); + + coded = palloc0(plan.total); + memcpy(coded, itup, plan.hoff); + v = coded + plan.hoff; + if (plan.vsize <= VARATT_SHORT_MAX) + { + SET_VARSIZE_SHORT(v, plan.vsize); + payload = v + VARHDRSZ_SHORT; + } + else + { + SET_VARSIZE(v, plan.vsize); + payload = v + VARHDRSZ; + } + payload[0] = (char) plan.code; + memcpy(payload + 1, plan.suffix, plan.suffixlen); + if (plan.code & BTREE_PREFIX_REST) + { + Size restorig = plan.hoff + plan.c1; + + memcpy(coded + _bt_prefix_restoff(plan.hoff + plan.vsize, restorig), + (char *) itup + restorig, plan.keyend - restorig); + } + memcpy(coded + plan.codedkeyend, (char *) itup + plan.keyend, + IndexTupleSize(itup) - plan.keyend); + + ((IndexTuple) coded)->t_info = + (itup->t_info & ~INDEX_SIZE_MASK) | (unsigned short) plan.total; + if (BTreeTupleIsPosting(itup)) + BTreeTupleSetPosting((IndexTuple) coded, BTreeTupleGetNPosting(itup), + plan.codedkeyend); + +#ifdef USE_ASSERT_CHECKING + { + BTItemBuf check; + IndexTuple decoded = _bt_prefix_decode(page, (IndexTuple) coded, + &check); + + Assert(IndexTupleSize(decoded) == IndexTupleSize(itup) && + memcmp(decoded, itup, IndexTupleSize(itup)) == 0); + } +#endif + + return (IndexTuple) coded; +} + +/* The MAXALIGNed size `itup' takes on `page', coded if the page codes it. */ +Size +_bt_prefix_coded_size(Page page, IndexTuple itup) +{ + const char *prefix; + Size prefixlen; + BTPrefixPlan plan; + + if (!P_HASPREFIX(BTPageGetOpaque(page)) || + !_bt_prefix_codable(itup)) + return MAXALIGN(IndexTupleSize(itup)); + + prefix = _bt_page_get_prefix(page, &prefixlen); + _bt_prefix_plan(prefix, prefixlen, itup, &plan); + return MAXALIGN(plan.total); +} + +/* + * Decode the item `itup' of the BTP_PREFIX page `page' into `buf', and return + * it; an item stored plain is returned as it is. The inverse of + * _bt_prefix_encode, byte for byte. An item that does not decode within its + * own bounds, or claims more of the prefix than the page has, is reported as + * corruption. + */ +IndexTuple +_bt_prefix_decode(Page page, IndexTuple itup, BTItemBuf *buf) +{ + const char *prefix; + Size prefixlen; + Size size = IndexTupleSize(itup); + Size hoff; + Size codedkeyend; + const char *v; + Size vsize = 0; + const char *payload; + Size paylen = 0; + uint8 code; + Size shared; + char *out = buf->data; + char *val = NULL; + Size c1; + Size restorig; + Size keyend; + + if (!_bt_prefix_codable(itup)) + return itup; + + prefix = _bt_page_get_prefix(page, &prefixlen); + hoff = IndexInfoFindDataOffset(itup->t_info); + codedkeyend = _bt_prefix_keyend(itup); + v = (const char *) itup + hoff; + if (hoff >= codedkeyend || codedkeyend > size || + (vsize = VARSIZE_ANY(v)) > codedkeyend - hoff || + (paylen = VARSIZE_ANY_EXHDR(v)) < 1) + goto corrupt; + payload = VARDATA_ANY(v); + code = (uint8) payload[0]; + shared = code & BTREE_PREFIX_SHARED_MASK; + + _bt_prefix_copy(out, (const char *) itup, hoff); + val = out + hoff; + if (shared == BTREE_PREFIX_RAW) + { + c1 = paylen - 1; + _bt_prefix_copy(val, payload + 1, c1); + if (c1 < VARHDRSZ_SHORT || VARSIZE_ANY(val) != c1) + goto corrupt; + } + else + { + Size datalen = shared + paylen - 1; + char *data; + + if (hoff + VARHDRSZ + datalen > sizeof(BTItemBuf)) + goto corrupt; + if (shared > prefixlen) + ereport(ERROR, + (errcode(ERRCODE_INDEX_CORRUPTED), + errmsg_internal("btree leaf tuple shares %zu bytes of a %zu-byte page prefix", + shared, prefixlen))); + if (datalen + VARHDRSZ_SHORT <= VARATT_SHORT_MAX) + { + c1 = datalen + VARHDRSZ_SHORT; + SET_VARSIZE_SHORT(val, c1); + data = val + VARHDRSZ_SHORT; + } + else + { + c1 = datalen + VARHDRSZ; + SET_VARSIZE(val, c1); + data = val + VARHDRSZ; + } + _bt_prefix_copy(data, prefix, shared); + _bt_prefix_copy(data + shared, payload + 1, paylen - 1); + } + + restorig = hoff + c1; + if (code & BTREE_PREFIX_REST) + { + Size restoff = _bt_prefix_restoff(hoff + vsize, restorig); + + if (restoff > codedkeyend) + goto corrupt; + keyend = restorig + (codedkeyend - restoff); + if (keyend + (size - codedkeyend) > sizeof(BTItemBuf)) + goto corrupt; + _bt_prefix_copy(out + restorig, (const char *) itup + restoff, + codedkeyend - restoff); + } + else + { + keyend = MAXALIGN(restorig); + if (keyend + (size - codedkeyend) > sizeof(BTItemBuf)) + goto corrupt; + memset(out + restorig, 0, keyend - restorig); + } + _bt_prefix_copy(out + keyend, (const char *) itup + codedkeyend, + size - codedkeyend); + + ((IndexTuple) out)->t_info = (itup->t_info & ~INDEX_SIZE_MASK) | + (unsigned short) (keyend + (size - codedkeyend)); + if (BTreeTupleIsPosting(itup)) + BTreeTupleSetPosting((IndexTuple) out, BTreeTupleGetNPosting(itup), + keyend); + return (IndexTuple) out; + +corrupt: + ereport(ERROR, + (errcode(ERRCODE_INDEX_CORRUPTED), + errmsg_internal("btree leaf tuple is not a valid prefix-coded tuple"))); + return NULL; /* keep compiler quiet */ +} + +/* + * The bytes of `itup''s first column a page prefix may be taken from, and how + * many (at most BTREE_PREFIX_MAX) in *len; NULL when the tuple has none: it + * is not codable, its value is empty, or it would be stored raw. + */ +const char * +_bt_prefix_candidate(IndexTuple itup, Size *len) +{ + const char *val; + + if (!_bt_prefix_codable(itup)) + return NULL; + val = (const char *) itup + IndexInfoFindDataOffset(itup->t_info); + if (!_bt_prefix_canonical(val) || VARSIZE_ANY_EXHDR(val) == 0) + return NULL; + *len = Min(VARSIZE_ANY_EXHDR(val), BTREE_PREFIX_MAX); + return VARDATA_ANY(val); +} + +/* + * How many bytes of `prefix' the first column of `itup' would share on a page + * with that prefix: -1 when the tuple would not be coded, 0 when it would be + * stored raw. + */ +int +_bt_prefix_shared(const char *prefix, Size prefixlen, IndexTuple itup) +{ + BTPrefixPlan plan; + + if (!_bt_prefix_codable(itup)) + return -1; + _bt_prefix_plan(prefix, prefixlen, itup, &plan); + if ((plan.code & BTREE_PREFIX_SHARED_MASK) == BTREE_PREFIX_RAW) + return 0; + return plan.code & BTREE_PREFIX_SHARED_MASK; +} + +/* + * May the leaves of `index' have a prefix? Only when the prefix_compression + * reloption is on and the first key column is of a varlena type. + */ +bool +_bt_prefix_enabled(Relation index) +{ + return BTGetPrefixCompression(index) && + TupleDescAttr(RelationGetDescr(index), 0)->attlen == -1; +} + +/* + * Choose the prefix for a leaf about to be built from the `n' tuples `items', + * in key order: the first tuple's first-column bytes, cut to the longest run + * any later tuple shares, provided the tuples share at least + * BTREE_PREFIX_MIN_SHARED bytes of it on average. Returns the prefix's + * length, with *prefix pointing into items[0], or 0 for no prefix. + */ +Size +_bt_prefix_choose(Relation index, IndexTuple *items, int n, + const char **prefix) +{ + const char *cand; + Size candlen; + Size longest = 0; + Size total = 0; + int ncoded = 0; + + if (!_bt_prefix_enabled(index) || + (cand = _bt_prefix_candidate(items[0], &candlen)) == NULL) + return 0; + if (n == 1) + { + *prefix = cand; + return candlen; + } + for (int i = 1; i < n; i++) + { + int shared = _bt_prefix_shared(cand, candlen, items[i]); + + if (shared < 0) + continue; + total += shared; + ncoded++; + longest = Max(longest, (Size) shared); + } + if (longest == 0 || total < (Size) BTREE_PREFIX_MIN_SHARED * ncoded) + return 0; + *prefix = cand; + return longest; +} + +/* + * Fault a freshly finished leaf `page' forward into the BTP_PREFIX format if + * a prefix pays off (Strategy 1: compress a page only when it is written as a + * whole -- CREATE INDEX page finish, a split half, a dedup rewrite -- so that + * no insert, VACUUM or key-range change ever forces a page rewrite, and an + * old-format page is converted only when it is next written whole). + * + * On entry `page' is a complete leaf: its high key at P_HIKEY if it is not + * rightmost, then its data items, none of them coded (the page does not yet + * have BTP_PREFIX). If the data items share enough of a common first-column + * prefix, the page is rebuilt in place with the PREFIX item inserted after + * the high key and every data item coded against it; otherwise the page is + * left unchanged. Item order and the high key are preserved. + * + * Caller must hold the page's buffer exclusively (or own the page outright, + * as the build and split paths do with a temp page). No WAL is written here: + * callers log the resulting page (a build writes it whole; a split logs each + * half whole or by full-page image), and redo reaches the same bytes by + * running _bt_prefix_encode against the same prefix. + */ +void +_bt_prefix_compress_page(Relation index, Page page) +{ + BTPageOpaque opaque = BTPageGetOpaque(page); + OffsetNumber minoff; + OffsetNumber maxoff; + int n; + IndexTuple *items; + Size prefixlen; + const char *prefix; + Page tmp; + OffsetNumber o; + IndexTuple hikey = NULL; + Size hikeysz = 0; + + Assert(P_ISLEAF(opaque) && !P_HASPREFIX(opaque)); + + if (!_bt_prefix_enabled(index)) + return; + + minoff = P_FIRSTDATAKEY(opaque); + maxoff = PageGetMaxOffsetNumber(page); + n = (int) maxoff - (int) minoff + 1; + if (n <= 0) + return; + + items = (IndexTuple *) palloc(n * sizeof(IndexTuple)); + for (int i = 0; i < n; i++) + items[i] = (IndexTuple) PageGetItem(page, + PageGetItemId(page, minoff + i)); + + prefixlen = _bt_prefix_choose(index, items, n, &prefix); + if (prefixlen == 0) + { + pfree(items); + return; + } + + /* The high key (if any) stays at P_HIKEY, uncoded. */ + if (!P_RIGHTMOST(opaque)) + { + ItemId hii = PageGetItemId(page, P_HIKEY); + + hikeysz = ItemIdGetLength(hii); + hikey = (IndexTuple) PageGetItem(page, hii); + } + + /* + * Rebuild into a temp page: high key (if any), PREFIX item, then each + * data item coded. The coded form can be up to MAXALIGN larger than the + * plain one, but the PREFIX item replaces far more shared bytes than it + * adds, so a page that was not overfull stays within bounds; if a coded + * item somehow does not fit, leave the page uncompressed (always valid). + */ + tmp = PageGetTempPageCopySpecial(page); + BTPageGetOpaque(tmp)->btpo_flags &= ~BTP_PREFIX; + if (hikey != NULL && + PageAddItem(tmp, (char *) hikey, hikeysz, P_HIKEY, false, false) == + InvalidOffsetNumber) + goto giveup; + _bt_page_set_prefix(tmp, prefix, prefixlen); + o = OffsetNumberNext(PageGetMaxOffsetNumber(tmp)); + for (int i = 0; i < n; i++) + { + IndexTuple coded = _bt_prefix_encode(tmp, items[i]); + bool ok; + + ok = PageAddItem(tmp, (char *) coded, IndexTupleSize(coded), o++, + false, false) != InvalidOffsetNumber; + if (coded != items[i]) + pfree(coded); + if (!ok) + goto giveup; + } + + memcpy(page, tmp, BLCKSZ); + pfree(tmp); + pfree(items); + return; + +giveup: + pfree(tmp); + pfree(items); +} + +/* + * Lay out one half of a leaf split on `page', which the caller has + * initialized (flags, links) and left empty: the high key `hikey' (NULL for + * a rightmost half), then the `n' plain tuples `items' in key order. + * + * The half is formed as a whole, so it may take a prefix of its own: its + * first item's leading bytes, if the items share enough (_bt_prefix_choose). + * Because a prefix-compressed leaf can hold more logical data than fits in a + * plain page, the half is laid out coded from the start rather than built + * plain and compressed afterwards. When the half does not fit with its own + * prefix (or shares too little to take one) it is laid out plain, and if that + * does not fit either, with `origprefix' (the splitting page's prefix, zero + * length when it had none), against which it fits by construction: the items + * keep the shares they had on the original page. + * + * Mirrors BARK's bark_split_leaf_half. Callers: _bt_split (both halves). + */ +void +_bt_split_leaf_half(Relation index, Page page, IndexTuple hikey, + IndexTuple *items, int n, + const char *origprefix, Size origprefixlen, + char *chosenprefix, Size *chosenprefixlen) +{ + BTPageOpaqueData saved = *BTPageGetOpaque(page); + const char *prefix[3]; + Size prefixlen[3]; + int ntries = 0; + + Assert(P_ISLEAF(&saved) && !P_HASPREFIX(&saved)); + + /* Candidate prefixes to try, in order: own, none, the orig page's. */ + if (_bt_prefix_enabled(index)) + { + prefixlen[ntries] = _bt_prefix_choose(index, items, n, &prefix[ntries]); + if (prefixlen[ntries] > 0) + ntries++; + } + prefix[ntries] = NULL; + prefixlen[ntries++] = 0; + if (origprefixlen > 0) + { + prefix[ntries] = origprefix; + prefixlen[ntries++] = origprefixlen; + } + + for (int t = 0; t < ntries; t++) + { + if (_bt_split_leaf_layout(page, hikey, items, n, prefix[t], prefixlen[t])) + { + /* Report the prefix that won, so the split can log it for redo. */ + if (chosenprefixlen != NULL) + { + *chosenprefixlen = prefixlen[t]; + if (prefixlen[t] > 0) + memcpy(chosenprefix, prefix[t], prefixlen[t]); + } + return; + } + /* restore the empty-page state for the next attempt */ + _bt_pageinit(page, BLCKSZ); + *BTPageGetOpaque(page) = saved; + } + elog(ERROR, "failed to lay out a split half of index \"%s\"", + RelationGetRelationName(index)); +} + +/* + * Lay out one leaf split half on the caller-initialized empty `page' using a + * GIVEN prefix (`prefixlen' 0 for none): the high key `hikey' (NULL for a + * rightmost half), the PREFIX item, then the `n' plain tuples `items' coded + * against the prefix. Returns false if they do not fit (caller tries another + * prefix). This is Relation-free and deterministic in (hikey, items, prefix), + * so WAL redo calls it with the logged prefix to reproduce the primary's page + * byte for byte. + */ +bool +_bt_split_leaf_layout(Page page, IndexTuple hikey, IndexTuple *items, int n, + const char *prefix, Size prefixlen) +{ + OffsetNumber o = P_HIKEY; + bool fit = true; + + Assert(P_ISLEAF(BTPageGetOpaque(page)) && !P_HASPREFIX(BTPageGetOpaque(page))); + if (hikey != NULL && + PageAddItem(page, (char *) hikey, MAXALIGN(IndexTupleSize(hikey)), + o++, false, false) == InvalidOffsetNumber) + return false; + if (prefixlen > 0) + { + _bt_page_set_prefix(page, prefix, prefixlen); + o++; + } + for (int i = 0; i < n; i++) + { + IndexTuple coded = _bt_prefix_encode(page, items[i]); + + if (PageAddItem(page, (char *) coded, IndexTupleSize(coded), + o++, false, false) == InvalidOffsetNumber) + fit = false; + if (coded != items[i]) + pfree(coded); + if (!fit) + return false; + } +#ifdef USE_ASSERT_CHECKING + if (P_HASPREFIX(BTPageGetOpaque(page))) + { + OffsetNumber mx = PageGetMaxOffsetNumber(page); + OffsetNumber off; + + for (off = P_FIRSTDATAKEY(BTPageGetOpaque(page)); off <= mx; off++) + { + BTItemBuf vb; + + (void) _bt_getitem(page, off, &vb); + } + } +#endif + return true; +} + +/* + * Collect the plain (decoded) data items of a leaf split into the left and + * right arrays, in the exact order and with the exact substitutions that the + * split performs: the original page's data items split at `firstrightoff', + * with `newitem' inserted at `newitemoff' on the side given by `newitemonleft' + * and, for a posting-list split, the posting at `origpagepostingoff' replaced + * by `nposting'. `origpage' is the pre-split page (its items are decoded when + * it is a BTP_PREFIX page). The arrays receive pointers that are valid until + * the caller is done laying out the halves; items taken from the page are + * copied, so the caller owns and should reset the surrounding memory context. + * + * Both the primary (_bt_split) and WAL redo (btree_xlog_split) collect items + * this way and then lay out each half with _bt_split_leaf_half, so the two + * produce byte-identical coded pages without logging a full-page image. + */ +void +_bt_split_leaf_collect(Page origpage, OffsetNumber firstrightoff, + OffsetNumber newitemoff, IndexTuple newitem, + bool newitemonleft, OffsetNumber origpagepostingoff, + IndexTuple nposting, + IndexTuple *leftitems, int *nleft, + IndexTuple *rightitems, int *nright) +{ + BTPageOpaque oopaque = BTPageGetOpaque(origpage); + OffsetNumber maxoff = PageGetMaxOffsetNumber(origpage); + bool hasprefix = P_HASPREFIX(oopaque); + OffsetNumber i; + + *nleft = 0; + *nright = 0; + for (i = P_FIRSTDATAKEY(oopaque); i <= maxoff; i = OffsetNumberNext(i)) + { + ItemId itemid = PageGetItemId(origpage, i); + IndexTuple dataitem = (IndexTuple) PageGetItem(origpage, itemid); + BTItemBuf dibuf; + + if (hasprefix) + dataitem = _bt_prefix_decode(origpage, dataitem, &dibuf); + + if (i == origpagepostingoff) + dataitem = nposting; + else if (i == newitemoff) + { + if (newitemonleft) + leftitems[(*nleft)++] = newitem; + else + rightitems[(*nright)++] = newitem; + } + + if (i < firstrightoff) + leftitems[(*nleft)++] = CopyIndexTuple(dataitem); + else + rightitems[(*nright)++] = CopyIndexTuple(dataitem); + } + if (i <= newitemoff) + rightitems[(*nright)++] = newitem; +} diff --git a/src/backend/access/nbtree/nbtreadpage.c b/src/backend/access/nbtree/nbtreadpage.c index 55722fd8791..ffc5846e4c9 100644 --- a/src/backend/access/nbtree/nbtreadpage.c +++ b/src/backend/access/nbtree/nbtreadpage.c @@ -145,6 +145,8 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum, ignore_killed_tuples = scan->ignore_killed_tuples; int itemIndex, indnatts; + BTItemBuf itupbuf; /* decode workspace for a loop's current item */ + BTItemBuf finalbuf; /* decode workspace for a backward finaltup */ /* save the page/buffer block number, along with its sibling links */ page = BufferGetPage(so->currPos.buf); @@ -252,7 +254,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum, continue; } - itup = (IndexTuple) PageGetItem(page, iid); + itup = _bt_getitem(page, offnum, &itupbuf); Assert(!BTreeTupleIsPivot(itup)); pstate.offnum = offnum; @@ -353,9 +355,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum, { if (minoff <= maxoff && !P_LEFTMOST(opaque)) { - ItemId iid = PageGetItemId(page, minoff); - - pstate.finaltup = (IndexTuple) PageGetItem(page, iid); + pstate.finaltup = _bt_getitem(page, minoff, &finalbuf); if (unlikely(so->scanBehind) && !_bt_scanbehind_checkkeys(scan, dir, pstate.finaltup)) @@ -415,7 +415,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum, else tuple_alive = true; - itup = (IndexTuple) PageGetItem(page, iid); + itup = _bt_getitem(page, offnum, &itupbuf); Assert(!BTreeTupleIsPivot(itup)); pstate.offnum = offnum; @@ -596,9 +596,10 @@ _bt_set_startikey(IndexScanDesc scan, BTReadPageState *pstate) BTScanOpaque so = (BTScanOpaque) scan->opaque; Relation rel = scan->indexRelation; TupleDesc tupdesc = RelationGetDescr(rel); - ItemId iid; IndexTuple firsttup, lasttup; + BTItemBuf firstbuf, + lastbuf; int startikey = 0, arrayidx = 0, firstchangingattnum; @@ -616,12 +617,10 @@ _bt_set_startikey(IndexScanDesc scan, BTReadPageState *pstate) return; /* minoff is an offset to the lowest non-pivot tuple on the page */ - iid = PageGetItemId(pstate->page, pstate->minoff); - firsttup = (IndexTuple) PageGetItem(pstate->page, iid); + firsttup = _bt_getitem(pstate->page, pstate->minoff, &firstbuf); /* maxoff is an offset to the highest non-pivot tuple on the page */ - iid = PageGetItemId(pstate->page, pstate->maxoff); - lasttup = (IndexTuple) PageGetItem(pstate->page, iid); + lasttup = _bt_getitem(pstate->page, pstate->maxoff, &lastbuf); /* Determine the first attribute whose values change on caller's page */ firstchangingattnum = _bt_keep_natts_fast(rel, firsttup, lasttup); @@ -2053,6 +2052,7 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate, ScanDirection dir = pstate->dir; OffsetNumber aheadoffnum; IndexTuple ahead; + BTItemBuf aheadbuf; /* decode workspace for the look-ahead tuple */ Assert(!pstate->forcenonrequired); @@ -2088,8 +2088,7 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate, aheadoffnum = Max((int) pstate->minoff, (int) pstate->offnum - pstate->targetdistance); - ahead = (IndexTuple) PageGetItem(pstate->page, - PageGetItemId(pstate->page, aheadoffnum)); + ahead = _bt_getitem(pstate->page, aheadoffnum, &aheadbuf); if (_bt_tuple_before_array_skeys(scan, dir, ahead, tupdesc, tupnatts, false, 0, NULL)) { diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c index 0abdd7b49f5..b8b8ec018f3 100644 --- a/src/backend/access/nbtree/nbtree.c +++ b/src/backend/access/nbtree/nbtree.c @@ -435,8 +435,10 @@ btrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys, */ if (scan->xs_want_itup && so->currTuples == NULL) { - so->currTuples = (char *) palloc(BLCKSZ * 2); - so->markTuples = so->currTuples + BLCKSZ; + Size half = BTTupleStorageSize(scan->indexRelation); + + so->currTuples = (char *) palloc(half * 2); + so->markTuples = so->currTuples + half; } /* diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c index 61f63251c6f..f932ca33c3a 100644 --- a/src/backend/access/nbtree/nbtsearch.c +++ b/src/backend/access/nbtree/nbtsearch.c @@ -696,6 +696,7 @@ _bt_compare(Relation rel, TupleDesc itupdesc = RelationGetDescr(rel); BTPageOpaque opaque = BTPageGetOpaque(page); IndexTuple itup; + BTItemBuf itupbuf; ItemPointer heapTid; ScanKey scankey; int ncmpkey; @@ -713,7 +714,7 @@ _bt_compare(Relation rel, if (!P_ISLEAF(opaque) && offnum == P_FIRSTDATAKEY(opaque)) return 1; - itup = (IndexTuple) PageGetItem(page, PageGetItemId(page, offnum)); + itup = _bt_getitem(page, offnum, &itupbuf); ntupatts = BTreeTupleGetNAtts(itup, rel); /* diff --git a/src/backend/access/nbtree/nbtsort.c b/src/backend/access/nbtree/nbtsort.c index 756dfa3dcf4..9a8b372033e 100644 --- a/src/backend/access/nbtree/nbtsort.c +++ b/src/backend/access/nbtree/nbtsort.c @@ -241,6 +241,10 @@ typedef struct BTPageState Size btps_lastextra; /* last item's extra posting list space */ uint32 btps_level; /* tree level (0 = leaf) */ Size btps_full; /* "full" if less than this much free space */ + /* Leaf prefix compression: the candidate prefix of the page being filled */ + const char *btps_prefix; /* into the page's first data item, or NULL */ + Size btps_prefixlen; /* its length (0 = no candidate) */ + Size btps_codedsaved; /* bytes the page's items would save coded */ struct BTPageState *btps_next; /* link to parent level, if any */ } BTPageState; @@ -856,7 +860,8 @@ _bt_buildadd(BTWriteState *wstate, BTPageState *state, IndexTuple itup, */ Assert(last_truncextra == 0 || isleaf); if (pgspc < itupsz + (isleaf ? MAXALIGN(sizeof(ItemPointerData)) : 0) || - (pgspc + last_truncextra < state->btps_full && last_off > P_FIRSTKEY)) + (pgspc + last_truncextra + state->btps_codedsaved < state->btps_full && + last_off > P_FIRSTKEY)) { /* * Finish off the page and write it out. @@ -980,6 +985,15 @@ _bt_buildadd(BTWriteState *wstate, BTPageState *state, IndexTuple itup, nopaque->btpo_next = P_NONE; /* redundant */ } + /* + * The old leaf page is complete: fault it forward into the + * BTP_PREFIX format if a prefix pays off. Done after btps_lowkey + * (the downlink, a copy of the uncoded high key) is saved, since + * compression rewrites opage in place. + */ + if (isleaf) + _bt_prefix_compress_page(wstate->index, opage); + /* * Write out the old page. _bt_blwritepage takes ownership of the * 'opage' buffer. @@ -990,6 +1004,11 @@ _bt_buildadd(BTWriteState *wstate, BTPageState *state, IndexTuple itup, * Reset last_off to point to new page */ last_off = P_FIRSTKEY; + + /* The new page starts with no prefix candidate and no savings yet. */ + state->btps_prefix = NULL; + state->btps_prefixlen = 0; + state->btps_codedsaved = 0; } /* @@ -1017,6 +1036,34 @@ _bt_buildadd(BTWriteState *wstate, BTPageState *state, IndexTuple itup, _bt_sortaddtup(npage, itupsz, itup, last_off, !isleaf && last_off == P_FIRSTKEY); + /* + * Leaf prefix compression accounting (Strategy 1): the page's eventual + * prefix is its first data item's first-column bytes; estimate how much + * room coding this page's items would free, so the fillfactor test above + * can pack more plain items onto a page that will shrink when it is + * compressed at finish. Items are still placed plain here; the estimate + * only relaxes the soft limit, never the hard fit check. + */ + if (isleaf && _bt_prefix_enabled(wstate->index)) + { + if (state->btps_prefix == NULL) + state->btps_prefix = _bt_prefix_candidate(itup, + &state->btps_prefixlen); + else + { + int shared = _bt_prefix_shared(state->btps_prefix, + state->btps_prefixlen, itup); + + /* + * A coded item drops `shared' value bytes but keeps a 1-byte + * share count; count the net MAXALIGN-rounded saving. + */ + if (shared > 1) + state->btps_codedsaved += + MAXALIGN(itupsz) - MAXALIGN(itupsz - (shared - 1)); + } + } + state->btps_buf = nbuf; state->btps_blkno = nblkno; state->btps_lastoff = last_off; @@ -1115,6 +1162,11 @@ _bt_uppershutdown(BTWriteState *wstate, BTPageState *state) * back one slot. Then we can dump out the page. */ _bt_slideleft((Page) s->btps_buf); + + /* Fault the finished rightmost leaf forward into BTP_PREFIX if it pays. */ + if (s->btps_level == 0) + _bt_prefix_compress_page(wstate->index, (Page) s->btps_buf); + _bt_blwritepage(wstate, s->btps_buf, s->btps_blkno); s->btps_buf = NULL; /* writepage took ownership of the buffer */ } diff --git a/src/backend/access/nbtree/nbtsplitloc.c b/src/backend/access/nbtree/nbtsplitloc.c index de9eca3c8b2..14bee568e45 100644 --- a/src/backend/access/nbtree/nbtsplitloc.c +++ b/src/backend/access/nbtree/nbtsplitloc.c @@ -52,6 +52,9 @@ typedef struct int rightspace; /* space available for items on right page */ int olddataitemstotal; /* space taken by old items */ Size minfirstrightsz; /* smallest firstright size */ + bool hasprefix; /* origpage is prefix-compressed (BTP_PREFIX) */ + BTItemBuf lbuf; /* decode scratch for _bt_split_lastleft */ + BTItemBuf rbuf; /* decode scratch for _bt_split_firstright */ /* candidate split point data */ int maxsplits; /* maximum number of splits */ @@ -172,6 +175,23 @@ _bt_findsplitloc(Relation rel, olddataitemstotal = rightspace - (int) PageGetExactFreeSpace(origpage); leaffillfactor = BTGetFillFactor(rel); + /* + * On a prefix-compressed leaf the page carries an extra PREFIX item that + * is not a data item. The split is reasoned about in CODED (on-page) + * bytes -- the physical reality of the page and of each recompressed half + * -- so subtract only the PREFIX item's space from olddataitemstotal and + * use the on-page (coded) item sizes in the scan below. (Plain-size + * accounting is not usable here: a compressed leaf can hold far more + * logical data than two plain pages, so a plain split can be infeasible.) + */ + if (P_HASPREFIX(opaque)) + { + ItemId pii = PageGetItemId(origpage, BTPageGetPrefixOff(opaque)); + + olddataitemstotal -= (int) (MAXALIGN(ItemIdGetLength(pii)) + + sizeof(ItemIdData)); + } + /* Passed-in newitemsz is MAXALIGNED but does not include line pointer */ newitemsz += sizeof(ItemIdData); state.rel = rel; @@ -185,6 +205,7 @@ _bt_findsplitloc(Relation rel, state.olddataitemstotal = olddataitemstotal; state.minfirstrightsz = SIZE_MAX; state.newitemoff = newitemoff; + state.hasprefix = P_HASPREFIX(opaque); /* newitem cannot be a posting list item */ Assert(!BTreeTupleIsPosting(newitem)); @@ -463,6 +484,19 @@ _bt_recsplitloc(FindSplitData *state, newitemisfirstright = (firstrightoff == state->newitemoff && !newitemonleft); + /* + * Reject a split that would leave the left page with no data items at all + * (firstrightoff at the first data key, with newitem not on the left). + * Upstream's split-point heuristics never choose such a point for a plain + * page; on a prefix-compressed page the coded-size accounting can make it + * look attractive, and _bt_split cannot form a left high key from a + * nonexistent lastleft. + */ + if (state->hasprefix && + firstrightoff == P_FIRSTDATAKEY(BTPageGetOpaque(state->origpage)) && + !newitemonleft) + return; + if (newitemisfirstright) firstrightsz = state->newitemsz; else @@ -686,8 +720,12 @@ _bt_afternewitemoff(FindSplitData *state, OffsetNumber maxoff, */ if (state->newitemoff > maxoff) { + BTItemBuf tbuf; + itemid = PageGetItemId(state->origpage, maxoff); tup = (IndexTuple) PageGetItem(state->origpage, itemid); + if (state->hasprefix) + tup = _bt_prefix_decode(state->origpage, tup, &tbuf); keepnatts = _bt_keep_natts_fast(state->rel, tup, state->newitem); if (keepnatts > 1 && keepnatts <= nkeyatts) @@ -714,12 +752,20 @@ _bt_afternewitemoff(FindSplitData *state, OffsetNumber maxoff, */ itemid = PageGetItemId(state->origpage, OffsetNumberPrev(state->newitemoff)); tup = (IndexTuple) PageGetItem(state->origpage, itemid); - /* Do cheaper test first */ + /* Do cheaper test first (t_tid and the posting bit survive coding) */ if (BTreeTupleIsPosting(tup) || !_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); + if (state->hasprefix) + { + BTItemBuf tbuf; + + tup = _bt_prefix_decode(state->origpage, tup, &tbuf); + keepnatts = _bt_keep_natts_fast(state->rel, tup, state->newitem); + } + else + keepnatts = _bt_keep_natts_fast(state->rel, tup, state->newitem); if (keepnatts > 1 && keepnatts <= nkeyatts) { @@ -1160,12 +1206,38 @@ static inline IndexTuple _bt_split_lastleft(FindSplitData *state, SplitPoint *split) { ItemId itemid; + OffsetNumber off; if (split->newitemonleft && split->firstrightoff == state->newitemoff) return state->newitem; - itemid = PageGetItemId(state->origpage, - OffsetNumberPrev(split->firstrightoff)); + off = OffsetNumberPrev(split->firstrightoff); + + /* + * On a prefix-compressed leaf the PREFIX item sits just before the first + * data item. When the split boundary is at the first data key, the data + * predecessor is the high key on a plain page (a pivot, read plain); a + * rightmost page has none, so fall back to the first data item (this only + * feeds penalty estimation, never placement). Never read the PREFIX item + * as a data tuple. + */ + if (state->hasprefix) + { + BTPageOpaque op = BTPageGetOpaque(state->origpage); + + if (off == BTPageGetPrefixOff(op)) + { + if (!P_RIGHTMOST(op)) + return (IndexTuple) PageGetItem(state->origpage, + PageGetItemId(state->origpage, P_HIKEY)); + off = P_FIRSTDATAKEY(op); + } + itemid = PageGetItemId(state->origpage, off); + return _bt_prefix_decode(state->origpage, + (IndexTuple) PageGetItem(state->origpage, itemid), + &state->lbuf); + } + itemid = PageGetItemId(state->origpage, off); return (IndexTuple) PageGetItem(state->origpage, itemid); } @@ -1181,5 +1253,9 @@ _bt_split_firstright(FindSplitData *state, SplitPoint *split) return state->newitem; itemid = PageGetItemId(state->origpage, split->firstrightoff); + if (state->hasprefix) + return _bt_prefix_decode(state->origpage, + (IndexTuple) PageGetItem(state->origpage, itemid), + &state->rbuf); return (IndexTuple) PageGetItem(state->origpage, itemid); } diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c index 85c1b632522..5dc2b2f3050 100644 --- a/src/backend/access/nbtree/nbtutils.c +++ b/src/backend/access/nbtree/nbtutils.c @@ -602,7 +602,9 @@ btoptions(Datum reloptions, bool validate) {"vacuum_cleanup_index_scale_factor", RELOPT_TYPE_REAL, offsetof(BTOptions, vacuum_cleanup_index_scale_factor)}, {"deduplicate_items", RELOPT_TYPE_BOOL, - offsetof(BTOptions, deduplicate_items)} + offsetof(BTOptions, deduplicate_items)}, + {"prefix_compression", RELOPT_TYPE_BOOL, + offsetof(BTOptions, prefix_compression)} }; return (bytea *) build_reloptions(reloptions, validate, diff --git a/src/backend/access/nbtree/nbtxlog.c b/src/backend/access/nbtree/nbtxlog.c index dff7d286fc8..098c785d4d8 100644 --- a/src/backend/access/nbtree/nbtxlog.c +++ b/src/backend/access/nbtree/nbtxlog.c @@ -243,6 +243,209 @@ btree_xlog_insert(bool isleaf, bool ismeta, bool posting, _bt_restore_meta(record, 2); } +/* + * Replay a prefix-compressed leaf page split. + * + * Rather than replay logged tuples (whose coded physical layout + * _bt_restore_page could not reproduce), rebuild both halves the way the + * primary did: collect the split's plain items from the pre-split original + * page (block 0) with _bt_split_leaf_collect, then lay each half out coded + * against its chosen prefix (carried in the record) with _bt_split_leaf_layout. + * The result is byte-identical to the primary, so no full-page image is + * needed. The record's block-0 data carries, in order: newitem (when it went + * on the left, or for a posting split), the left high key, and the left half's + * prefix bytes; block-1 data carries the right half's prefix bytes. + */ +static void +btree_xlog_split_prefix(XLogReaderState *record, BlockNumber origpagenumber, + BlockNumber rightpagenumber, BlockNumber spagenumber, + bool newitemonleft) +{ + XLogRecPtr lsn = record->EndRecPtr; + xl_btree_split *xlrec = (xl_btree_split *) XLogRecGetData(record); + Buffer rbuf; + Buffer origbuf; + Page origpage; + BTPageOpaque oopaque; + char *datapos; + Size datalen; + IndexTuple newitem = NULL; + IndexTuple left_hikey; + IndexTuple right_hikey = NULL; + IndexTuple nposting = NULL; + Size newitemsz = 0; + const char *leftprefix = NULL; + const char *rightprefix = NULL; + IndexTuple rightitems[MaxIndexTuplesPerPage]; + int nright = 0; + Page rightpage; + BTPageOpaque ropaque; + XLogRedoAction origaction; + + /* + * The right (new) page is rebuilt entirely from data logged with the + * record, independent of block 0, because block 0 may be restored from an + * automatic full-page image (so its live contents after redo are the left + * half, not the pre-split page). Block-1 data is, in order: the right + * half's chosen prefix bytes (rightprefixlen), the right high key when the + * split is not rightmost (spagenumber != P_NONE), then the right half's + * plain data items in offset order (self-describing by IndexTupleSize). + */ + datapos = XLogRecGetBlockData(record, 1, &datalen); + if (xlrec->flags & XLH_SPLIT_RFPI) + { + /* + * The right half's plain items would have overflowed the WAL record, + * so the primary carried the coded right page as a forced full-page + * image. Restore it from that image. + */ + if (XLogReadBufferForRedo(record, 1, &rbuf) != BLK_RESTORED) + elog(ERROR, "prefix leaf split right page lacks its full-page image"); + } + else + { + if (xlrec->rightprefixlen > 0) + { + rightprefix = datapos; + datapos += xlrec->rightprefixlen; + datalen -= xlrec->rightprefixlen; + } + if (spagenumber != P_NONE) + { + right_hikey = (IndexTuple) datapos; + datapos += MAXALIGN(IndexTupleSize(right_hikey)); + datalen -= MAXALIGN(IndexTupleSize(right_hikey)); + } + while (datalen > 0) + { + IndexTuple it = (IndexTuple) datapos; + Size sz = MAXALIGN(IndexTupleSize(it)); + + rightitems[nright++] = it; + datapos += sz; + datalen -= sz; + } + + rbuf = XLogInitBufferForRedo(record, 1); + rightpage = BufferGetPage(rbuf); + _bt_pageinit(rightpage, BufferGetPageSize(rbuf)); + ropaque = BTPageGetOpaque(rightpage); + ropaque->btpo_prev = origpagenumber; + ropaque->btpo_next = spagenumber; + ropaque->btpo_level = xlrec->level; + ropaque->btpo_flags = BTP_LEAF; + ropaque->btpo_cycleid = 0; + if (!_bt_split_leaf_layout(rightpage, right_hikey, rightitems, nright, + rightprefix, xlrec->rightprefixlen)) + elog(ERROR, "failed to replay right half of a prefix leaf split"); + PageSetLSN(rightpage, lsn); + } + MarkBufferDirty(rbuf); + + /* + * Rebuild the left (original) page from the pre-split page, unless it was + * restored from a full-page image (then it is already the final left + * half). Block-0 data is: [newitem (left/posting)] [left high key] + * [left prefix]. The left plain items come from the pre-split page. + */ + origaction = XLogReadBufferForRedo(record, 0, &origbuf); + origpage = BufferGetPage(origbuf); + + if (origaction == BLK_NEEDS_REDO) + { + IndexTuple *leftitems; + int nleft; + OffsetNumber origpagepostingoff = InvalidOffsetNumber; + Page leftpage; + BTPageOpaque lopaque; + + oopaque = BTPageGetOpaque(origpage); + + datapos = XLogRecGetBlockData(record, 0, &datalen); + if (newitemonleft || xlrec->postingoff != 0) + { + newitem = (IndexTuple) datapos; + newitemsz = MAXALIGN(IndexTupleSize(newitem)); + datapos += newitemsz; + datalen -= newitemsz; + } + left_hikey = (IndexTuple) datapos; + datapos += MAXALIGN(IndexTupleSize(left_hikey)); + datalen -= MAXALIGN(IndexTupleSize(left_hikey)); + if (xlrec->leftprefixlen > 0) + { + leftprefix = datapos; + datapos += xlrec->leftprefixlen; + datalen -= xlrec->leftprefixlen; + } + Assert(datalen == 0); + + if (xlrec->postingoff != 0) + { + ItemId itemid; + IndexTuple oposting; + BTItemBuf obuf; + + origpagepostingoff = OffsetNumberPrev(xlrec->newitemoff); + newitem = CopyIndexTuple(newitem); + itemid = PageGetItemId(origpage, origpagepostingoff); + oposting = (IndexTuple) PageGetItem(origpage, itemid); + if (P_HASPREFIX(oopaque)) + oposting = _bt_prefix_decode(origpage, oposting, &obuf); + nposting = _bt_swap_posting(newitem, oposting, xlrec->postingoff); + } + + leftitems = palloc_array(IndexTuple, PageGetMaxOffsetNumber(origpage) + 2); + rightitems[0] = NULL; /* not reused below */ + { + IndexTuple dummyright[MaxIndexTuplesPerPage]; + int dummynr; + + _bt_split_leaf_collect(origpage, xlrec->firstrightoff, + xlrec->newitemoff, newitem, newitemonleft, + origpagepostingoff, nposting, + leftitems, &nleft, dummyright, &dummynr); + } + + leftpage = PageGetTempPageCopySpecial(origpage); + lopaque = BTPageGetOpaque(leftpage); + lopaque->btpo_flags = BTP_LEAF | BTP_INCOMPLETE_SPLIT; + lopaque->btpo_prev = oopaque->btpo_prev; + lopaque->btpo_next = rightpagenumber; + lopaque->btpo_level = xlrec->level; + lopaque->btpo_cycleid = 0; + if (!_bt_split_leaf_layout(leftpage, left_hikey, leftitems, nleft, + leftprefix, xlrec->leftprefixlen)) + elog(ERROR, "failed to replay left half of a prefix leaf split"); + PageRestoreTempPage(leftpage, origpage); + PageSetLSN(origpage, lsn); + MarkBufferDirty(origbuf); + pfree(leftitems); + } + + /* Fix left-link of the page to the right of the new right sibling. */ + if (spagenumber != P_NONE) + { + Buffer sbuf; + + if (XLogReadBufferForRedo(record, 2, &sbuf) == BLK_NEEDS_REDO) + { + Page spage = BufferGetPage(sbuf); + BTPageOpaque spageop = BTPageGetOpaque(spage); + + spageop->btpo_prev = rightpagenumber; + PageSetLSN(spage, lsn); + MarkBufferDirty(sbuf); + } + if (BufferIsValid(sbuf)) + UnlockReleaseBuffer(sbuf); + } + + UnlockReleaseBuffer(rbuf); + if (BufferIsValid(origbuf)) + UnlockReleaseBuffer(origbuf); +} + static void btree_xlog_split(bool newitemonleft, XLogReaderState *record) { @@ -264,6 +467,18 @@ btree_xlog_split(bool newitemonleft, XLogReaderState *record) if (!XLogRecGetBlockTagExtended(record, 2, NULL, NULL, &spagenumber, NULL)) spagenumber = P_NONE; + /* + * A prefix-compressed leaf split is replayed by rebuilding both halves + * from the pre-split original page, exactly as the primary did, so the + * coded pages come out byte-identical without a full-page image. + */ + if (xlrec->flags & XLH_SPLIT_LEAFPREFIX) + { + btree_xlog_split_prefix(record, origpagenumber, rightpagenumber, + spagenumber, newitemonleft); + return; + } + /* * Clear the incomplete split flag on the appropriate child page one level * down when origpage/buf is an internal page (there must have been @@ -280,7 +495,7 @@ btree_xlog_split(bool newitemonleft, XLogReaderState *record) if (!isleaf) _bt_clear_incomplete_split(record, 3); - /* Reconstruct right (new) sibling page from scratch */ + /* Reconstruct right (new) sibling page from the logged tuples. */ rbuf = XLogInitBufferForRedo(record, 1); datapos = XLogRecGetBlockData(record, 1, &datalen); rpage = BufferGetPage(rbuf); @@ -468,6 +683,54 @@ btree_xlog_dedup(XLogReaderState *record) BTDedupState state; BTDedupInterval *intervals; Page newpage; + bool pageprefix; /* source page is BTP_PREFIX */ + BTItemBuf curbuf; /* decode scratch for the current item */ + BTItemBuf basebuf; /* decode scratch kept for the pending base */ + + if (xlrec->prefixlen > 0) + { + /* + * Prefix-dedup: the deduped result was carried as the chosen prefix + * bytes, then (unless rightmost) the high key, then the deduped + * plain items. Rebuild the coded page byte-for-byte the way the + * primary did, using the same layout routine as a prefix leaf split. + */ + Size datalen; + char *datapos = XLogRecGetBlockData(record, 0, &datalen); + const char *prefix = datapos; + IndexTuple hikey = NULL; + IndexTuple items[MaxIndexTuplesPerPage]; + int nitems = 0; + + datapos += xlrec->prefixlen; + datalen -= xlrec->prefixlen; + if (xlrec->flags & XLH_DEDUP_NOT_RIGHTMOST) + { + hikey = (IndexTuple) datapos; + datapos += MAXALIGN(IndexTupleSize(hikey)); + datalen -= MAXALIGN(IndexTupleSize(hikey)); + } + while (datalen > 0) + { + IndexTuple it = (IndexTuple) datapos; + Size sz = MAXALIGN(IndexTupleSize(it)); + + items[nitems++] = it; + datapos += sz; + datalen -= sz; + } + + newpage = PageGetTempPageCopySpecial(page); + BTPageGetOpaque(newpage)->btpo_flags &= ~(BTP_PREFIX | BTP_HAS_GARBAGE); + if (!_bt_split_leaf_layout(newpage, hikey, items, nitems, + prefix, xlrec->prefixlen)) + elog(ERROR, "failed to replay prefix deduplication"); + PageRestoreTempPage(newpage, page); + PageSetLSN(page, lsn); + MarkBufferDirty(buf); + UnlockReleaseBuffer(buf); + return; + } state = palloc_object(BTDedupStateData); state->deduplicate = true; /* unused */ @@ -487,6 +750,19 @@ btree_xlog_dedup(XLogReaderState *record) maxoff = PageGetMaxOffsetNumber(page); newpage = PageGetTempPageCopySpecial(page); + /* + * The source page may be prefix-compressed even on the ordinary + * (non-prefix) dedup path: that happens when the primary deduplicated + * a prefix page but re-compression of the result declined, leaving a + * plain page (prefixlen == 0, no full-page image). Reproduce the + * primary: lay the result out plain and decode each coded data item + * before feeding it to the dedup state machine. The high key is a + * pivot and is never coded. + */ + pageprefix = P_HASPREFIX(opaque); + if (pageprefix) + BTPageGetOpaque(newpage)->btpo_flags &= ~BTP_PREFIX; + if (!P_RIGHTMOST(opaque)) { ItemId itemid = PageGetItemId(page, P_HIKEY); @@ -505,8 +781,18 @@ btree_xlog_dedup(XLogReaderState *record) ItemId itemid = PageGetItemId(page, offnum); IndexTuple itup = (IndexTuple) PageGetItem(page, itemid); + /* Decode on a prefix source so keys/posting lists are real. */ + if (pageprefix) + itup = _bt_prefix_decode(page, itup, &curbuf); + if (offnum == minoff) + { + /* base must survive the whole run; keep it in its own buffer */ + if (pageprefix) + itup = _bt_prefix_decode(page, (IndexTuple) PageGetItem(page, itemid), + &basebuf); _bt_dedup_start_pending(state, itup, offnum); + } else if (state->nintervals < xlrec->nintervals && state->baseoff == intervals[state->nintervals].baseoff && state->nitems < intervals[state->nintervals].nitems) @@ -516,12 +802,15 @@ btree_xlog_dedup(XLogReaderState *record) } else { - _bt_dedup_finish_pending(newpage, state); + _bt_dedup_finish_pending(newpage, state, false); + if (pageprefix) + itup = _bt_prefix_decode(page, (IndexTuple) PageGetItem(page, itemid), + &basebuf); _bt_dedup_start_pending(state, itup, offnum); } } - _bt_dedup_finish_pending(newpage, state); + _bt_dedup_finish_pending(newpage, state, false); Assert(state->nintervals == xlrec->nintervals); Assert(memcmp(state->intervals, intervals, state->nintervals * sizeof(BTDedupInterval)) == 0); @@ -621,11 +910,23 @@ btree_xlog_vacuum(XLogReaderState *record) if (xlrec->ndeleted > 0) PageIndexMultiDelete(page, (OffsetNumber *) ptr, xlrec->ndeleted); + opaque = BTPageGetOpaque(page); + + /* + * Mirror the primary: an emptied prefix-compressed leaf drops its + * PREFIX item and the BTP_PREFIX flag (see _bt_delitems_vacuum). + */ + if (P_HASPREFIX(opaque) && + PageGetMaxOffsetNumber(page) == BTPageGetPrefixOff(opaque)) + { + PageIndexTupleDelete(page, BTPageGetPrefixOff(opaque)); + opaque->btpo_flags &= ~BTP_PREFIX; + } + /* * Clear the vacuum cycle ID, and mark the page as not containing any * LP_DEAD items */ - opaque = BTPageGetOpaque(page); opaque->btpo_cycleid = 0; opaque->btpo_flags &= ~BTP_HAS_GARBAGE; diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h index 3097e9bb1af..ca7e6852542 100644 --- a/src/include/access/nbtree.h +++ b/src/include/access/nbtree.h @@ -83,6 +83,8 @@ typedef BTPageOpaqueData *BTPageOpaque; #define BTP_HAS_GARBAGE (1 << 6) /* page has LP_DEAD tuples (deprecated) */ #define BTP_INCOMPLETE_SPLIT (1 << 7) /* right sibling's downlink is missing */ #define BTP_HAS_FULLXID (1 << 8) /* contains BTDeletedPageData */ +#define BTP_PREFIX (1 << 9) /* leaf keys coded against a page prefix + * (see nbtprefix.c) */ /* * The max allowed value of a cycle ID is a bit less than 64K. This is @@ -367,7 +369,61 @@ typedef struct BTVacState #define P_HIKEY ((OffsetNumber) 1) #define P_FIRSTKEY ((OffsetNumber) 2) -#define P_FIRSTDATAKEY(opaque) (P_RIGHTMOST(opaque) ? P_HIKEY : P_FIRSTKEY) + +/* + * Leaf key prefix compression (see nbtprefix.c for the format and code). + * + * A leaf of an index whose prefix_compression reloption is on, and whose + * first key column is of a varlena type, may be flagged BTP_PREFIX. Such a + * page holds one extra item, the PREFIX item, at BTPageGetPrefixOff (where + * its first data item would otherwise be); its data items start one offset + * later, so P_FIRSTDATAKEY -- which every loop over a page's data items + * starts from -- accounts for the flag. The PREFIX item is an + * IndexTupleData header (t_tid zero, t_info its size and nothing else, so + * that code walking a page's items by IndexTupleSize, such as split redo, + * steps over it) followed by 1..BTREE_PREFIX_MAX prefix bytes. The prefix is + * chosen only when a page is written whole (CREATE INDEX, or a half of a + * split) and never changes after that; a page's high key, which fixes the + * prefix item's offset, also changes only in a split. + * + * On a BTP_PREFIX page, every non-pivot tuple (plain or posting) whose first + * key column is not NULL stores that column coded, as a varlena (1-byte + * header when it fits one, else 4-byte) whose payload starts with a uint8 + * `shared': + * + * shared <= BTREE_PREFIX_MAX: the value's data bytes are the page prefix's + * first `shared' bytes followed by the rest of the payload. + * The value's own varlena header is recomputed on decode (the + * form index_form_tuple gives it: 1-byte when it fits, else + * 4-byte uncompressed). + * shared == BTREE_PREFIX_RAW: the rest of the payload is the value exactly + * as stored, header included. Used for a value compressed + * inline, or whose header is not the canonical form above. + * + * The bytes after the first column (later key columns, INCLUDE columns, the + * posting list, and alignment padding) are kept as they are, moved to keep + * their alignment, or left out when they are only the key's closing zero + * padding (decode puts them back). A posting list follows the key at the new + * key end, and the posting offset in t_tid is changed to match. Pivots, NULL + * first columns, and the high key are stored as on any other page. + * + * Coding and decoding depend only on the page's prefix and the tuple, not on + * the catalog, so WAL redo can repeat them, and decoding gives back the plain + * tuple byte for byte. + */ +#define BTREE_PREFIX_MAX 126 +#define BTREE_PREFIX_RAW 127 /* shared count of a raw value */ +#define BTREE_PREFIX_SHARED_MASK 0x7F /* shared count bits */ +#define BTREE_PREFIX_REST 0x80 /* bytes after the column follow */ + +/* A leaf whose tuples share less of its prefix than this, on average, has none. */ +#define BTREE_PREFIX_MIN_SHARED 4 + +#define P_HASPREFIX(opaque) (((opaque)->btpo_flags & BTP_PREFIX) != 0) +#define BTPageGetPrefixOff(opaque) \ + (P_RIGHTMOST(opaque) ? P_HIKEY : P_FIRSTKEY) +#define P_FIRSTDATAKEY(opaque) \ + (BTPageGetPrefixOff(opaque) + (P_HASPREFIX(opaque) ? 1 : 0)) /* * Notes on B-Tree tuple format, and key and non-key attributes: @@ -676,6 +732,36 @@ BTreeTupleGetMaxHeapTID(IndexTuple itup) return &itup->t_tid; } +/* + * Leaf key prefix compression (see nbtprefix.c for the format). + * + * Workspace for _bt_getitem. Every read of a leaf DATA item (any item but + * the high key or a pivot) that could be on a BTP_PREFIX page goes through + * that function rather than bare PageGetItem, so decoding has one home. The + * workspace holds a decoded copy; the returned tuple is valid until the + * workspace is reused or the page is unlocked. High keys and pivots, and + * items a caller changes in place, are read with PageGetItem. + */ +typedef PGAlignedBlock BTItemBuf; + +extern IndexTuple _bt_prefix_decode(Page page, IndexTuple itup, BTItemBuf *buf); + +/* + * Return the data item at `offnum' on `page' as a plain IndexTuple: on a + * BTP_PREFIX page, the tuple decoded into `buf'; otherwise the on-page tuple, + * and `buf' is not used. Callers must not write through the result. + */ +static inline IndexTuple +_bt_getitem(Page page, OffsetNumber offnum, BTItemBuf *buf) +{ + IndexTuple itup = (IndexTuple) PageGetItem(page, PageGetItemId(page, offnum)); + BTPageOpaque opaque = BTPageGetOpaque(page); + + if (unlikely(P_HASPREFIX(opaque)) && offnum > BTPageGetPrefixOff(opaque)) + return _bt_prefix_decode(page, itup, buf); + return itup; +} + /* * Operator strategy numbers for B-tree have been moved to access/stratnum.h, * because many places need to use them in ScanKeyInit() calls. @@ -1122,6 +1208,7 @@ typedef struct BTOptions int fillfactor; /* page fill factor in percent (0..100) */ float8 vacuum_cleanup_index_scale_factor; /* deprecated */ bool deduplicate_items; /* Try to deduplicate items? */ + bool prefix_compression; /* Code leaf keys against a page prefix? */ } BTOptions; #define BTGetFillFactor(relation) \ @@ -1137,6 +1224,33 @@ typedef struct BTOptions relation->rd_rel->relam == BTREE_AM_OID), \ ((relation)->rd_options ? \ ((BTOptions *) (relation)->rd_options)->deduplicate_items : true)) +#define BTGetPrefixCompression(relation) \ + (AssertMacro(relation->rd_rel->relkind == RELKIND_INDEX && \ + relation->rd_rel->relam == BTREE_AM_OID), \ + ((relation)->rd_options ? \ + ((BTOptions *) (relation)->rd_options)->prefix_compression : false)) + +/* + * Size of one half (currTuples or markTuples) of an index-only scan's tuple + * storage. A plain leaf's live non-pivot tuples fit in one page, so BLCKSZ + * would suffice for them. A prefix-compressed (BTP_PREFIX) leaf can hold more + * logical data than fits plain: when its tuples are decoded for the scan they + * can exceed BLCKSZ. Because _bt_getitem() decodes based on the page flag, + * not on the current reloption, and a BTP_PREFIX page can persist after + * prefix_compression is turned off (the flag is cleared only when a page is + * split, emptied or dedup-rewritten -- never by ALTER INDEX), ANY btree index + * may contain such a page. The buffer must therefore always reserve the slack + * for a decoded page, independent of the live reloption; sizing it on the + * option would overflow currTuples/markTuples on a surviving prefix leaf. + * + * The bound is the most distinct index tuples a leaf can hold (each at least a + * line pointer plus a minimally coded tuple) times the most a single first + * column can regain on decode (the page prefix, at most BTREE_PREFIX_MAX + * bytes, MAXALIGNed). + */ +#define BTTupleStorageSize(relation) \ + (BLCKSZ + (BLCKSZ / (int) MAXALIGN(sizeof(IndexTupleData) + 2)) * \ + (int) MAXALIGN(BTREE_PREFIX_MAX)) /* * Constant definition for progress reporting. Phase numbers must match @@ -1202,7 +1316,8 @@ extern bool _bt_bottomupdel_pass(Relation rel, Buffer buf, Relation heapRel, extern void _bt_dedup_start_pending(BTDedupState state, IndexTuple base, OffsetNumber baseoff); extern bool _bt_dedup_save_htid(BTDedupState state, IndexTuple itup); -extern Size _bt_dedup_finish_pending(Page newpage, BTDedupState state); +extern Size _bt_dedup_finish_pending(Page newpage, BTDedupState state, + bool overflowok); extern IndexTuple _bt_form_posting(IndexTuple base, const ItemPointerData *htids, int nhtids); extern void _bt_update_posting(BTVacuumPosting vacposting); @@ -1315,6 +1430,34 @@ extern void _bt_check_third_page(Relation rel, Relation heap, bool needheaptidspace, Page page, IndexTuple newtup); extern bool _bt_allequalimage(Relation rel, bool debugmessage); +/* + * prototypes for functions in nbtprefix.c + */ +extern const char *_bt_page_get_prefix(Page page, Size *len); +extern void _bt_page_set_prefix(Page page, const char *prefix, Size len); +extern IndexTuple _bt_prefix_encode(Page page, IndexTuple itup); +extern Size _bt_prefix_coded_size(Page page, IndexTuple itup); +extern bool _bt_prefix_enabled(Relation index); +extern const char *_bt_prefix_candidate(IndexTuple itup, Size *len); +extern int _bt_prefix_shared(const char *prefix, Size prefixlen, IndexTuple itup); +extern Size _bt_prefix_choose(Relation index, IndexTuple *items, int n, + const char **prefix); +extern void _bt_prefix_compress_page(Relation index, Page page); +extern void _bt_split_leaf_half(Relation index, Page page, IndexTuple hikey, + IndexTuple *items, int n, + const char *origprefix, Size origprefixlen, + char *chosenprefix, Size *chosenprefixlen); +extern bool _bt_split_leaf_layout(Page page, IndexTuple hikey, + IndexTuple *items, int n, + const char *prefix, Size prefixlen); +extern void _bt_split_leaf_collect(Page origpage, OffsetNumber firstrightoff, + OffsetNumber newitemoff, IndexTuple newitem, + bool newitemonleft, + OffsetNumber origpagepostingoff, + IndexTuple nposting, + IndexTuple *leftitems, int *nleft, + IndexTuple *rightitems, int *nright); + /* * prototypes for functions in nbtvalidate.c */ diff --git a/src/include/access/nbtxlog.h b/src/include/access/nbtxlog.h index 3a78ec27fe8..285eac6f1aa 100644 --- a/src/include/access/nbtxlog.h +++ b/src/include/access/nbtxlog.h @@ -156,25 +156,59 @@ typedef struct xl_btree_split OffsetNumber firstrightoff; /* first origpage item on rightpage */ OffsetNumber newitemoff; /* new item's offset */ uint16 postingoff; /* offset inside orig posting tuple */ + uint16 flags; /* XLH_SPLIT_* below */ + uint8 leftprefixlen; /* leaf: left half's prefix length (0 none) */ + uint8 rightprefixlen; /* leaf: right half's prefix length (0 none) */ } xl_btree_split; -#define SizeOfBtreeSplit (offsetof(xl_btree_split, postingoff) + sizeof(uint16)) +#define SizeOfBtreeSplit (offsetof(xl_btree_split, rightprefixlen) + sizeof(uint8)) + +/* + * xl_btree_split flag bits. + * + * A prefix-compressed leaf split logs no full-page image in the common case: + * redo rebuilds each half the way the primary did. The left half is + * reconstructed from the pre-split original page (block 0) together with the + * logged new item and high key; the right half is reconstructed from its plain + * data items, which are logged as block-1 data. Each half's chosen prefix + * bytes are logged too (block 0 for the left half, block 1 for the right), and + * applied with _bt_split_leaf_layout(); redo cannot run the prefix-choosing + * heuristic itself, so the primary logs the prefix it chose. + * leftprefixlen/rightprefixlen give the lengths (0 means the half is not + * prefix-compressed). XLH_SPLIT_LEAFPREFIX marks a leaf split that uses this + * path at all (so redo rebuilds coded rather than replaying items). + */ +#define XLH_SPLIT_LEAFPREFIX 0x01 /* leaf split, rebuild halves coded */ +#define XLH_SPLIT_RFPI 0x08 /* right half carried by a full-page image + * (its plain items would overflow the WAL + * record); redo restores it from the image */ /* * When page is deduplicated, consecutive groups of tuples with equal keys are * merged together into posting list tuples. * - * The WAL record represents a deduplication pass for a leaf page. An array - * of BTDedupInterval structs follows. + * The WAL record represents a deduplication pass for a leaf page. Normally + * (prefixlen == 0) an array of BTDedupInterval structs follows and redo + * replays them against the old page. When the page is prefix-compressed + * (prefixlen > 0) the deduped result is instead carried as the chosen prefix + * bytes (prefixlen) followed by, when not rightmost, the high key, then the + * deduped plain data items -- each MAXALIGN'd -- and redo lays them out coded + * with _bt_split_leaf_layout(), avoiding a full-page image. */ typedef struct xl_btree_dedup { uint16 nintervals; + uint8 prefixlen; /* >0: prefix-dedup payload, see above */ + uint8 flags; /* XLH_DEDUP_* */ - /* DEDUPLICATION INTERVALS FOLLOW */ + /* DEDUPLICATION INTERVALS or PREFIX PAYLOAD FOLLOW */ } xl_btree_dedup; -#define SizeOfBtreeDedup (offsetof(xl_btree_dedup, nintervals) + sizeof(uint16)) +#define SizeOfBtreeDedup (offsetof(xl_btree_dedup, flags) + sizeof(uint8)) + +/* xl_btree_dedup flag bits */ +#define XLH_DEDUP_NOT_RIGHTMOST 0x01 /* prefix payload includes a high key */ +#define XLH_DEDUP_RFPI 0x02 /* prefix page carried as a forced image */ /* * This is what we need to know about page reuse within btree. This record diff --git a/src/test/regress/expected/btree_index.out b/src/test/regress/expected/btree_index.out index 7c53d598649..c1dae4b55ad 100644 --- a/src/test/regress/expected/btree_index.out +++ b/src/test/regress/expected/btree_index.out @@ -891,3 +891,259 @@ ALTER INDEX btree_part_idx ALTER COLUMN id SET (n_distinct=100); ERROR: ALTER action ALTER COLUMN ... SET cannot be performed on relation "btree_part_idx" DETAIL: This operation is not supported for partitioned indexes. DROP TABLE btree_part; +-- +-- Leaf key prefix compression (prefix_compression reloption). +-- +-- Correctness is checked against an independent oracle: a forced index scan +-- over a prefix-compressed index must return exactly the rows a sequential +-- scan of the same data returns, across build, retail insert/split, delete +-- and VACUUM, for equality, range, backward, index-only and NULL queries. +-- The key column uses the C collation so the recorded output is deterministic +-- across platforms. The page format is exercised by keys that share a long +-- prefix (URL-like), by keys sharing exactly 125/126/127 bytes (the boundary +-- where the one-byte shared count saturates: BTREE_PREFIX_MAX is 126, +-- BTREE_PREFIX_RAW is 127), by keys that share nothing, and by a NULL key. +-- +CREATE TABLE btree_prefix (u text COLLATE "C"); +-- Build-path pages (CREATE INDEX writes whole leaves). A shared 40-byte +-- prefix, plus explicit rows at the 125/126/127 shared-byte boundary. +INSERT INTO btree_prefix + SELECT 'https://example.com/path/to/resource/' || lpad((g % 20000)::text, 8, '0') + FROM generate_series(1, 20000) g; +INSERT INTO btree_prefix + SELECT repeat('a', 40) || repeat('b', n) FROM generate_series(85, 90) n; +INSERT INTO btree_prefix VALUES + (repeat('a', 125)), (repeat('a', 126)), (repeat('a', 127)), (repeat('a', 300)), + ('zzz-no-shared-prefix'), (NULL); +CREATE INDEX btree_prefix_on ON btree_prefix (u) WITH (prefix_compression = on); +-- Confirm the index really is used (and is prefix-compressed), so the queries +-- below exercise the coded page format rather than silently falling back. +SET enable_seqscan = off; +SET enable_bitmapscan = off; +EXPLAIN (costs off) + SELECT u FROM btree_prefix WHERE u = 'https://example.com/path/to/resource/00012345'; + QUERY PLAN +--------------------------------------------------------------------------- + Index Only Scan using btree_prefix_on on btree_prefix + Index Cond: (u = 'https://example.com/path/to/resource/00012345'::text) +(2 rows) + +-- Equality, range and backward scans forced through the prefix index. Their +-- results are recorded in the expected output, which is itself the oracle: a +-- decode regression changes these rows. (The index-only digest below adds an +-- oracle that does not read the index at all.) +SELECT count(*) FROM btree_prefix + WHERE u = 'https://example.com/path/to/resource/00012345'; + count +------- + 1 +(1 row) + +SELECT count(*) FROM btree_prefix + WHERE u BETWEEN 'https://example.com/path/to/resource/00000100' + AND 'https://example.com/path/to/resource/00000900'; + count +------- + 801 +(1 row) + +SELECT u FROM btree_prefix + WHERE u > 'https://example.com/path/to/resource/00019996' ORDER BY u DESC; + u +----------------------------------------------- + zzz-no-shared-prefix + https://example.com/path/to/resource/00019999 + https://example.com/path/to/resource/00019998 + https://example.com/path/to/resource/00019997 +(4 rows) + +-- Boundary and oversized keys round-trip to their exact stored values. +SELECT u FROM btree_prefix WHERE u LIKE 'aaaa%' ORDER BY u; + u +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +(10 rows) + +SELECT count(*) FROM btree_prefix WHERE u = repeat('a', 300); + count +------- + 1 +(1 row) + +SELECT count(*) FROM btree_prefix WHERE u IS NULL; + count +------- + 1 +(1 row) + +-- Index-only scan over the prefix index, compared to the literal values that +-- were inserted (an oracle that does not itself read the index). +SELECT (SELECT md5(string_agg(u, '|' ORDER BY u)) + FROM btree_prefix WHERE u IS NOT NULL) = + md5(string_agg(x, '|' ORDER BY x)) AS ios_ok + FROM ( + SELECT 'https://example.com/path/to/resource/' || lpad((g % 20000)::text, 8, '0') AS x + FROM generate_series(1, 20000) g + UNION ALL SELECT repeat('a', 40) || repeat('b', n) FROM generate_series(85, 90) n + UNION ALL SELECT v FROM (VALUES (repeat('a',125)),(repeat('a',126)), + (repeat('a',127)),(repeat('a',300)), + ('zzz-no-shared-prefix')) t(v) + ) oracle; + ios_ok +-------- + t +(1 row) + +-- Retail inserts cause splits that fault pages forward into the coded format; +-- an index-only scan must still agree with the independent oracle afterwards. +INSERT INTO btree_prefix + SELECT 'https://example.com/other/' || lpad(g::text, 8, '0') + FROM generate_series(1, 10000) g; +-- Delete then VACUUM, then re-insert into freed space. The delete targets +-- only part of the URL namespace so the boundary keys (aaaa.../zzz...) survive +-- for the checks below. +DELETE FROM btree_prefix + WHERE u >= 'https://example.com/path/to/resource/00000000' + AND u < 'https://example.com/path/to/resource/00005000'; +VACUUM btree_prefix; +INSERT INTO btree_prefix + SELECT 'https://example.com/path/to/resource/' || lpad((g % 5000)::text, 8, '0') + FROM generate_series(1, 5000) g; +-- After the churn, an index-only scan of the live keys must equal the same +-- keys fetched from the heap by a sequential scan (an independent path): if +-- decoding the coded leaves were wrong, the two digests would differ. +SET enable_indexonlyscan = on; +SELECT md5(string_agg(u, '|' ORDER BY u)) AS ios_digest + FROM btree_prefix WHERE u IS NOT NULL \gset +SET enable_indexscan = off; +SET enable_indexonlyscan = off; +SET enable_seqscan = on; +SELECT :'ios_digest' = + (SELECT md5(string_agg(u, '|' ORDER BY u)) FROM btree_prefix WHERE u IS NOT NULL) + AS post_churn_ok; + post_churn_ok +--------------- + t +(1 row) + +SET enable_indexscan = on; +RESET enable_seqscan; +RESET enable_bitmapscan; +-- The reloption round-trips and can be toggled without a rewrite. +SELECT reloptions FROM pg_class WHERE relname = 'btree_prefix_on'; + reloptions +------------------------- + {prefix_compression=on} +(1 row) + +ALTER INDEX btree_prefix_on SET (prefix_compression = off); +SELECT reloptions FROM pg_class WHERE relname = 'btree_prefix_on'; + reloptions +-------------------------- + {prefix_compression=off} +(1 row) + +-- Reading back with the option off still returns correct rows: existing +-- BTP_PREFIX leaves keep their coded form and decode on the page flag. +SET enable_seqscan = off; +SELECT count(*) FROM btree_prefix WHERE u = repeat('a', 126); + count +------- + 1 +(1 row) + +RESET enable_seqscan; +-- A non-varlena first column is legal and silently stores no prefix; verify a +-- scan over such an index returns correct rows. +CREATE INDEX btree_prefix_int ON btree_prefix ((length(u))) WITH (prefix_compression = on); +SET enable_seqscan = off; +SELECT length(u) FROM btree_prefix WHERE length(u) = 300; + length +-------- + 300 +(1 row) + +RESET enable_seqscan; +DROP TABLE btree_prefix; +-- +-- Bottom-up index deletion on prefix-compressed pages. +-- +-- An UPDATE that does not change the indexed column marks the resulting index +-- insert "logically unchanged", which lets nbtree run a bottom-up deletion +-- pass before splitting a leaf. That pass reads and (only) removes items; it +-- must work on coded pages too. Here we exercise the path and verify the +-- index stays correct; committed update batches between VACUUMs leave +-- removable duplicates for the pass to find. (Bloat is quantified in the +-- performance benchmark, not here, since removability is timing-dependent.) +-- +CREATE TABLE btree_prefix_bu (id int PRIMARY KEY, u text COLLATE "C", filler text); +INSERT INTO btree_prefix_bu + SELECT g, 'https://example.com/path/' || lpad((g % 500)::text, 8, '0'), repeat('x', 40) + FROM generate_series(1, 4000) g; +CREATE INDEX btree_prefix_bu_u ON btree_prefix_bu (u) WITH (prefix_compression = on); +-- Several committed non-key UPDATE batches (version churn on the prefix index). +UPDATE btree_prefix_bu SET filler = repeat('y', 40); +UPDATE btree_prefix_bu SET filler = repeat('z', 40); +UPDATE btree_prefix_bu SET filler = repeat('w', 40); +-- Correctness after the churn: an index-only scan of the key column must equal +-- the independent oracle (the literal keys), proving no decode/deletion damage. +SET enable_seqscan = off; +SELECT (SELECT md5(string_agg(u, '|' ORDER BY u)) FROM btree_prefix_bu) = + md5(string_agg(x, '|' ORDER BY x)) AS bottomup_ok + FROM (SELECT 'https://example.com/path/' || lpad((g % 500)::text, 8, '0') AS x + FROM generate_series(1, 4000) g) oracle; + bottomup_ok +------------- + t +(1 row) + +SELECT count(*) FROM btree_prefix_bu + WHERE u = 'https://example.com/path/' || lpad('7', 8, '0'); + count +------- + 8 +(1 row) + +RESET enable_seqscan; +DROP TABLE btree_prefix_bu; +-- +-- Runtime deduplication on a prefix-compressed page. Duplicate keys inserted +-- one at a time into an existing prefix index fill a leaf; before it splits, +-- the deduplication pass decodes the coded items, merges the duplicate TIDs +-- into posting lists and re-codes the page. The decoded set can exceed one +-- page even when the coded form fit, in which case the pass aborts and the +-- page splits instead; either way the result must be correct. +-- +CREATE TABLE btree_prefix_dd (u text COLLATE "C"); +CREATE INDEX btree_prefix_dd_u ON btree_prefix_dd (u) WITH (prefix_compression = on); +INSERT INTO btree_prefix_dd + SELECT 'https://example.com/path/to/resource/' || lpad((g % 200)::text, 8, '0') + FROM generate_series(1, 40000) g; +SET enable_seqscan = off; +SELECT (SELECT md5(string_agg(u, '|' ORDER BY u)) FROM btree_prefix_dd) = + md5(string_agg(x, '|' ORDER BY x)) AS runtime_dedup_ok + FROM (SELECT 'https://example.com/path/to/resource/' || lpad((g % 200)::text, 8, '0') AS x + FROM generate_series(1, 40000) g) oracle; + runtime_dedup_ok +------------------ + t +(1 row) + +SELECT count(*) FROM btree_prefix_dd + WHERE u = 'https://example.com/path/to/resource/' || lpad('42', 8, '0'); + count +------- + 200 +(1 row) + +RESET enable_seqscan; +DROP TABLE btree_prefix_dd; diff --git a/src/test/regress/sql/btree_index.sql b/src/test/regress/sql/btree_index.sql index 6990d7818da..6cd053a1596 100644 --- a/src/test/regress/sql/btree_index.sql +++ b/src/test/regress/sql/btree_index.sql @@ -520,3 +520,170 @@ CREATE TABLE btree_part (id int4) PARTITION BY RANGE (id); CREATE INDEX btree_part_idx ON btree_part(id); ALTER INDEX btree_part_idx ALTER COLUMN id SET (n_distinct=100); DROP TABLE btree_part; + +-- +-- Leaf key prefix compression (prefix_compression reloption). +-- +-- Correctness is checked against an independent oracle: a forced index scan +-- over a prefix-compressed index must return exactly the rows a sequential +-- scan of the same data returns, across build, retail insert/split, delete +-- and VACUUM, for equality, range, backward, index-only and NULL queries. +-- The key column uses the C collation so the recorded output is deterministic +-- across platforms. The page format is exercised by keys that share a long +-- prefix (URL-like), by keys sharing exactly 125/126/127 bytes (the boundary +-- where the one-byte shared count saturates: BTREE_PREFIX_MAX is 126, +-- BTREE_PREFIX_RAW is 127), by keys that share nothing, and by a NULL key. +-- +CREATE TABLE btree_prefix (u text COLLATE "C"); +-- Build-path pages (CREATE INDEX writes whole leaves). A shared 40-byte +-- prefix, plus explicit rows at the 125/126/127 shared-byte boundary. +INSERT INTO btree_prefix + SELECT 'https://example.com/path/to/resource/' || lpad((g % 20000)::text, 8, '0') + FROM generate_series(1, 20000) g; +INSERT INTO btree_prefix + SELECT repeat('a', 40) || repeat('b', n) FROM generate_series(85, 90) n; +INSERT INTO btree_prefix VALUES + (repeat('a', 125)), (repeat('a', 126)), (repeat('a', 127)), (repeat('a', 300)), + ('zzz-no-shared-prefix'), (NULL); +CREATE INDEX btree_prefix_on ON btree_prefix (u) WITH (prefix_compression = on); + +-- Confirm the index really is used (and is prefix-compressed), so the queries +-- below exercise the coded page format rather than silently falling back. +SET enable_seqscan = off; +SET enable_bitmapscan = off; +EXPLAIN (costs off) + SELECT u FROM btree_prefix WHERE u = 'https://example.com/path/to/resource/00012345'; + +-- Equality, range and backward scans forced through the prefix index. Their +-- results are recorded in the expected output, which is itself the oracle: a +-- decode regression changes these rows. (The index-only digest below adds an +-- oracle that does not read the index at all.) +SELECT count(*) FROM btree_prefix + WHERE u = 'https://example.com/path/to/resource/00012345'; +SELECT count(*) FROM btree_prefix + WHERE u BETWEEN 'https://example.com/path/to/resource/00000100' + AND 'https://example.com/path/to/resource/00000900'; +SELECT u FROM btree_prefix + WHERE u > 'https://example.com/path/to/resource/00019996' ORDER BY u DESC; +-- Boundary and oversized keys round-trip to their exact stored values. +SELECT u FROM btree_prefix WHERE u LIKE 'aaaa%' ORDER BY u; +SELECT count(*) FROM btree_prefix WHERE u = repeat('a', 300); +SELECT count(*) FROM btree_prefix WHERE u IS NULL; + +-- Index-only scan over the prefix index, compared to the literal values that +-- were inserted (an oracle that does not itself read the index). +SELECT (SELECT md5(string_agg(u, '|' ORDER BY u)) + FROM btree_prefix WHERE u IS NOT NULL) = + md5(string_agg(x, '|' ORDER BY x)) AS ios_ok + FROM ( + SELECT 'https://example.com/path/to/resource/' || lpad((g % 20000)::text, 8, '0') AS x + FROM generate_series(1, 20000) g + UNION ALL SELECT repeat('a', 40) || repeat('b', n) FROM generate_series(85, 90) n + UNION ALL SELECT v FROM (VALUES (repeat('a',125)),(repeat('a',126)), + (repeat('a',127)),(repeat('a',300)), + ('zzz-no-shared-prefix')) t(v) + ) oracle; + +-- Retail inserts cause splits that fault pages forward into the coded format; +-- an index-only scan must still agree with the independent oracle afterwards. +INSERT INTO btree_prefix + SELECT 'https://example.com/other/' || lpad(g::text, 8, '0') + FROM generate_series(1, 10000) g; +-- Delete then VACUUM, then re-insert into freed space. The delete targets +-- only part of the URL namespace so the boundary keys (aaaa.../zzz...) survive +-- for the checks below. +DELETE FROM btree_prefix + WHERE u >= 'https://example.com/path/to/resource/00000000' + AND u < 'https://example.com/path/to/resource/00005000'; +VACUUM btree_prefix; +INSERT INTO btree_prefix + SELECT 'https://example.com/path/to/resource/' || lpad((g % 5000)::text, 8, '0') + FROM generate_series(1, 5000) g; +-- After the churn, an index-only scan of the live keys must equal the same +-- keys fetched from the heap by a sequential scan (an independent path): if +-- decoding the coded leaves were wrong, the two digests would differ. +SET enable_indexonlyscan = on; +SELECT md5(string_agg(u, '|' ORDER BY u)) AS ios_digest + FROM btree_prefix WHERE u IS NOT NULL \gset +SET enable_indexscan = off; +SET enable_indexonlyscan = off; +SET enable_seqscan = on; +SELECT :'ios_digest' = + (SELECT md5(string_agg(u, '|' ORDER BY u)) FROM btree_prefix WHERE u IS NOT NULL) + AS post_churn_ok; +SET enable_indexscan = on; +RESET enable_seqscan; +RESET enable_bitmapscan; + +-- The reloption round-trips and can be toggled without a rewrite. +SELECT reloptions FROM pg_class WHERE relname = 'btree_prefix_on'; +ALTER INDEX btree_prefix_on SET (prefix_compression = off); +SELECT reloptions FROM pg_class WHERE relname = 'btree_prefix_on'; +-- Reading back with the option off still returns correct rows: existing +-- BTP_PREFIX leaves keep their coded form and decode on the page flag. +SET enable_seqscan = off; +SELECT count(*) FROM btree_prefix WHERE u = repeat('a', 126); +RESET enable_seqscan; + +-- A non-varlena first column is legal and silently stores no prefix; verify a +-- scan over such an index returns correct rows. +CREATE INDEX btree_prefix_int ON btree_prefix ((length(u))) WITH (prefix_compression = on); +SET enable_seqscan = off; +SELECT length(u) FROM btree_prefix WHERE length(u) = 300; +RESET enable_seqscan; +DROP TABLE btree_prefix; + +-- +-- Bottom-up index deletion on prefix-compressed pages. +-- +-- An UPDATE that does not change the indexed column marks the resulting index +-- insert "logically unchanged", which lets nbtree run a bottom-up deletion +-- pass before splitting a leaf. That pass reads and (only) removes items; it +-- must work on coded pages too. Here we exercise the path and verify the +-- index stays correct; committed update batches between VACUUMs leave +-- removable duplicates for the pass to find. (Bloat is quantified in the +-- performance benchmark, not here, since removability is timing-dependent.) +-- +CREATE TABLE btree_prefix_bu (id int PRIMARY KEY, u text COLLATE "C", filler text); +INSERT INTO btree_prefix_bu + SELECT g, 'https://example.com/path/' || lpad((g % 500)::text, 8, '0'), repeat('x', 40) + FROM generate_series(1, 4000) g; +CREATE INDEX btree_prefix_bu_u ON btree_prefix_bu (u) WITH (prefix_compression = on); +-- Several committed non-key UPDATE batches (version churn on the prefix index). +UPDATE btree_prefix_bu SET filler = repeat('y', 40); +UPDATE btree_prefix_bu SET filler = repeat('z', 40); +UPDATE btree_prefix_bu SET filler = repeat('w', 40); +-- Correctness after the churn: an index-only scan of the key column must equal +-- the independent oracle (the literal keys), proving no decode/deletion damage. +SET enable_seqscan = off; +SELECT (SELECT md5(string_agg(u, '|' ORDER BY u)) FROM btree_prefix_bu) = + md5(string_agg(x, '|' ORDER BY x)) AS bottomup_ok + FROM (SELECT 'https://example.com/path/' || lpad((g % 500)::text, 8, '0') AS x + FROM generate_series(1, 4000) g) oracle; +SELECT count(*) FROM btree_prefix_bu + WHERE u = 'https://example.com/path/' || lpad('7', 8, '0'); +RESET enable_seqscan; +DROP TABLE btree_prefix_bu; + +-- +-- Runtime deduplication on a prefix-compressed page. Duplicate keys inserted +-- one at a time into an existing prefix index fill a leaf; before it splits, +-- the deduplication pass decodes the coded items, merges the duplicate TIDs +-- into posting lists and re-codes the page. The decoded set can exceed one +-- page even when the coded form fit, in which case the pass aborts and the +-- page splits instead; either way the result must be correct. +-- +CREATE TABLE btree_prefix_dd (u text COLLATE "C"); +CREATE INDEX btree_prefix_dd_u ON btree_prefix_dd (u) WITH (prefix_compression = on); +INSERT INTO btree_prefix_dd + SELECT 'https://example.com/path/to/resource/' || lpad((g % 200)::text, 8, '0') + FROM generate_series(1, 40000) g; +SET enable_seqscan = off; +SELECT (SELECT md5(string_agg(u, '|' ORDER BY u)) FROM btree_prefix_dd) = + md5(string_agg(x, '|' ORDER BY x)) AS runtime_dedup_ok + FROM (SELECT 'https://example.com/path/to/resource/' || lpad((g % 200)::text, 8, '0') AS x + FROM generate_series(1, 40000) g) oracle; +SELECT count(*) FROM btree_prefix_dd + WHERE u = 'https://example.com/path/to/resource/' || lpad('42', 8, '0'); +RESET enable_seqscan; +DROP TABLE btree_prefix_dd; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index a7a8dbdd23f..611c820d627 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -215,6 +215,7 @@ BTDeletedPageData BTIndexStat BTInsertState BTInsertStateData +BTItemBuf BTLeader BTMetaPageData BTOneVacInfo @@ -226,6 +227,7 @@ BTPageStat BTPageState BTParallelScanDesc BTPendingFSM +BTPrefixPlan BTReadPageState BTScanInsert BTScanInsertData -- 2.54.0