From 8daeae9a2faf53b92b8f61554226e6dcdf4109c9 Mon Sep 17 00:00:00 2001 From: Salma Date: Tue, 15 Sep 2026 12:55:14 +0300 Subject: [PATCH v3 2/3] pageinspect: Add support for B-tree page merges Extend pageinspect to display information about BTP_MERGED and BTP_MERGED_AWAY leaf pages in bt_page_stats() and bt_page_items(). Add new inspection functions: bt_find_merge_candidates() to locate mergeable leaf page pairs, bt_merge_detail() to inspect parent and sibling page states, and bt_merge() to trigger leaf page merges from SQL. --- contrib/pageinspect/btreefuncs.c | 800 +++++++++++++++++- .../pageinspect/pageinspect--1.12--1.13.sql | 63 ++ 2 files changed, 857 insertions(+), 6 deletions(-) diff --git a/contrib/pageinspect/btreefuncs.c b/contrib/pageinspect/btreefuncs.c index 9917663593b..8013faaac01 100644 --- a/contrib/pageinspect/btreefuncs.c +++ b/contrib/pageinspect/btreefuncs.c @@ -36,6 +36,9 @@ #include "funcapi.h" #include "miscadmin.h" #include "pageinspect.h" +#include "storage/block.h" +#include "storage/buf.h" +#include "storage/bufmgr.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/rel.h" @@ -48,10 +51,18 @@ PG_FUNCTION_INFO_V1(bt_page_items_bytea); PG_FUNCTION_INFO_V1(bt_page_stats_1_9); PG_FUNCTION_INFO_V1(bt_page_stats); PG_FUNCTION_INFO_V1(bt_multi_page_stats); +PG_FUNCTION_INFO_V1(bt_find_merge_candidates); +PG_FUNCTION_INFO_V1(bt_merge_detail); +PG_FUNCTION_INFO_V1(bt_merge); #define IS_INDEX(r) ((r)->rd_rel->relkind == RELKIND_INDEX) #define IS_BTREE(r) ((r)->rd_rel->relam == BTREE_AM_OID) +#define BTREE_MERGE_THRESHOLD 0.20 /* page must be ≤20% full to be a + * candidate */ +#define BTREE_TARGET_FILLFACTOR 0.90 /* merged result must stay ≤90% full */ + + /* ------------------------------------------------ * structure for single btree page statistics * ------------------------------------------------ @@ -98,6 +109,35 @@ typedef struct ua_page_items TupleDesc tupd; } ua_page_items; +/** + * cross-call data structure for SRF for merge candidates + */ +typedef struct ua_merge_candidates +{ + Oid relid; + BlockNumber current_blkno; + TupleDesc tupd; + float8 merge_candidate_max_pct; + float8 merge_destination_max_pct; + int num_pages; + int returned; + +} ua_merge_candidates; + +/* + * State for bt_merge_detail(): holds the three block numbers of the + * Parent / Left / Right page trio across SRF calls. + */ +typedef struct ua_merge_detail +{ + Oid relid; + BlockNumber parent_blkno; + BlockNumber left_blkno; + BlockNumber right_blkno; + int call_cntr; /* 0=parent, 1=left, 2=right */ + bool show_tids; + TupleDesc tupd; +} ua_merge_detail; /* ------------------------------------------------- * GetBTPageStatistics() @@ -156,6 +196,12 @@ GetBTPageStatistics(BlockNumber blkno, Buffer buffer, BTPageStat *stat) } else if (P_IGNORE(opaque)) stat->type = 'e'; + else if (P_ISMERGEDAWAY(opaque)) + { + stat->type = 'm'; + /* Don't interpret BTMergedAwayPageData as index tuples */ + maxoff = InvalidOffsetNumber; + } else if (P_ISLEAF(opaque)) stat->type = 'l'; else if (P_ISROOT(opaque)) @@ -672,12 +718,18 @@ bt_page_items_internal(PG_FUNCTION_ARGS, enum pageinspect_version ext_version) opaque = BTPageGetOpaque(uargs->page); - if (!P_ISDELETED(opaque)) + if (!P_ISDELETED(opaque) && !P_ISMERGEDAWAY(opaque)) fctx->max_calls = PageGetMaxOffsetNumber(uargs->page); else { - /* Don't interpret BTDeletedPageData as index tuples */ - elog(NOTICE, "page from block " INT64_FORMAT " is deleted", blkno); + /* + * Don't interpret BTDeletedPageData or BTMergedAwayPageData as + * index tuples + */ + if (P_ISDELETED(opaque)) + elog(NOTICE, "page from block " INT64_FORMAT " is deleted", blkno); + else + elog(NOTICE, "page from block " INT64_FORMAT " is merged away", blkno); fctx->max_calls = 0; } uargs->leafpage = P_ISLEAF(opaque); @@ -787,13 +839,21 @@ bt_page_items_bytea(PG_FUNCTION_ARGS) if (P_ISDELETED(opaque)) elog(NOTICE, "page is deleted"); + else if (P_ISMERGEDAWAY(opaque)) + elog(NOTICE, "page is merged away"); - if (!P_ISDELETED(opaque)) + if (!P_ISDELETED(opaque) && !P_ISMERGEDAWAY(opaque)) fctx->max_calls = PageGetMaxOffsetNumber(uargs->page); else { - /* Don't interpret BTDeletedPageData as index tuples */ - elog(NOTICE, "page from block is deleted"); + /* + * Don't interpret BTDeletedPageData or BTMergedAwayPageData as + * index tuples + */ + if (P_ISDELETED(opaque)) + elog(NOTICE, "page from block is deleted"); + else + elog(NOTICE, "page from block is merged away"); fctx->max_calls = 0; } uargs->leafpage = P_ISLEAF(opaque); @@ -935,3 +995,731 @@ bt_metap(PG_FUNCTION_ARGS) PG_RETURN_DATUM(result); } + + +/*------------------------------------------------------- + * bt_find_merge_candidates() + * + * Scan the B-tree leaf chain left-to-right and return up to num_pages + * LEFT block numbers of adjacent leaf pairs where both pages are at most + * min_pct_threshold percent full and their combined content fits within + * the target fillfactor. Only merge candidates are returned. + * + * Usage: SELECT * FROM bt_find_merge_candidates('t1_pkey', 50.0, 5); + *------------------------------------------------------- + */ +Datum +bt_find_merge_candidates(PG_FUNCTION_ARGS) +{ + text *relname = PG_GETARG_TEXT_PP(0); + float8 merge_candidate_max_pct = PG_GETARG_FLOAT8(1); + float8 merge_destination_max_pct = PG_GETARG_FLOAT8(2); + int32 num_pages = PG_GETARG_INT32(3); + Datum result; + FuncCallContext *fctx; + MemoryContext mctx; + ua_merge_candidates *uargs; + Buffer leftbuf, + rightbuf; + Page leftpage, + rightpage; + BTPageOpaque leftopaque, + rightopaque; + BlockNumber left_blkno, + right_blkno; + Size left_free, + right_free; + Size left_used, + right_used; + BTScanInsert scankey; + int j; + Datum values[5]; + bool nulls[5]; + HeapTuple tuple; + + if (!superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be superuser to use pageinspect functions"))); + + if (SRF_IS_FIRSTCALL()) + { + RangeVar *relrv; + Relation rel; + TupleDesc tupleDesc; + + fctx = SRF_FIRSTCALL_INIT(); + + relrv = makeRangeVarFromNameList(textToQualifiedNameList(relname)); + rel = relation_openrv(relrv, AccessShareLock); + + if (!IS_INDEX(rel) || !IS_BTREE(rel)) + ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a %s index", + RelationGetRelationName(rel), "btree"))); + + mctx = MemoryContextSwitchTo(fctx->multi_call_memory_ctx); + uargs = palloc_object(ua_merge_candidates); + + /* + * Start at the second leftmost leaf page. + */ + { + Buffer endpoint_buf = _bt_get_endpoint(rel, 0, false); + Page temp_page = BufferGetPage(endpoint_buf); + BTPageOpaque temp_opaque = BTPageGetOpaque(temp_page); + + uargs->current_blkno = temp_opaque->btpo_next; + /* elog(WARNING, "Hello from bt_find_merge_candidates"); */ + UnlockReleaseBuffer(endpoint_buf); + } + + uargs->relid = RelationGetRelid(rel); + relation_close(rel, AccessShareLock); + + uargs->merge_candidate_max_pct = merge_candidate_max_pct / 100.0; + uargs->merge_destination_max_pct = merge_destination_max_pct / 100.0; + uargs->num_pages = num_pages; + uargs->returned = 0; + + /* Build a tuple descriptor for our result type */ + if (get_call_result_type(fcinfo, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + tupleDesc = BlessTupleDesc(tupleDesc); + + uargs->tupd = tupleDesc; + fctx->user_fctx = uargs; + + MemoryContextSwitchTo(mctx); + } + + fctx = SRF_PERCALL_SETUP(); + uargs = fctx->user_fctx; + + if (uargs->returned >= uargs->num_pages) + SRF_RETURN_DONE(fctx); + + /* + * Reopen the relation for this call. We open and close it every call so + * that a LIMIT early exit cannot leak a relcache reference. + */ + { + Relation rel = relation_open(uargs->relid, AccessShareLock); + + /* + * Walk the leaf chain looking for the next merge candidate. We skip + * non-candidates and stop at the rightmost leaf or end of chain. + */ + for (;;) + { + CHECK_FOR_INTERRUPTS(); + + if (uargs->current_blkno == P_NONE) + { + relation_close(rel, AccessShareLock); + SRF_RETURN_DONE(fctx); + } + + left_blkno = uargs->current_blkno; + leftbuf = ReadBuffer(rel, left_blkno); + LockBuffer(leftbuf, BUFFER_LOCK_SHARE); + leftpage = BufferGetPage(leftbuf); + leftopaque = BTPageGetOpaque(leftpage); + + if (P_RIGHTMOST(leftopaque)) + { + UnlockReleaseBuffer(leftbuf); + relation_close(rel, AccessShareLock); + SRF_RETURN_DONE(fctx); + } + + /* Skip deleted, half-dead, and already-merged pages. */ + if (P_ISDELETED(leftopaque) || P_ISHALFDEAD(leftopaque) + || P_ISMERGED(leftopaque) || P_ISMERGEDAWAY(leftopaque)) + { + uargs->current_blkno = leftopaque->btpo_next; + UnlockReleaseBuffer(leftbuf); + continue; + } + + Assert(P_ISLEAF(leftopaque)); + + /* Lock couple: acquire right BEFORE releasing left */ + right_blkno = leftopaque->btpo_next; + left_free = PageGetFreeSpace(leftpage); + rightbuf = ReadBuffer(rel, right_blkno); + LockBuffer(rightbuf, BUFFER_LOCK_SHARE); + rightpage = BufferGetPage(rightbuf); + rightopaque = BTPageGetOpaque(rightpage); + right_free = PageGetFreeSpace(rightpage); + + /* R is unusable; skip directly past it using its btpo_next. */ + if (P_ISDELETED(rightopaque) || P_ISHALFDEAD(rightopaque) + || P_ISMERGED(rightopaque) || P_ISMERGEDAWAY(rightopaque)) + { + uargs->current_blkno = rightopaque->btpo_next; + UnlockReleaseBuffer(rightbuf); + UnlockReleaseBuffer(leftbuf); + continue; + } + + Assert(P_ISLEAF(rightopaque)); + + scankey = NULL; + { + OffsetNumber first_data_off = P_FIRSTDATAKEY(leftopaque); + + if (first_data_off <= PageGetMaxOffsetNumber(leftpage)) + { + IndexTuple first_itup = (IndexTuple) PageGetItem(leftpage, + PageGetItemId(leftpage, first_data_off)); + + scankey = _bt_mkscankey(rel, first_itup); + } + } + + /* L is examined; slide the window to R as the default next-left. */ + uargs->current_blkno = right_blkno; + + UnlockReleaseBuffer(rightbuf); + UnlockReleaseBuffer(leftbuf); + + left_used = BLCKSZ - left_free; + right_used = BLCKSZ - right_free; + + /* + * Size check: both pages must individually be below the threshold + * AND their combined content must fit within the target + * fillfactor. + */ + if ((float) left_used / BLCKSZ <= uargs->merge_candidate_max_pct && + (float) right_used / BLCKSZ <= uargs->merge_candidate_max_pct && + (float) (left_used + right_used) / BLCKSZ <= uargs->merge_destination_max_pct) + { + /* + * Parent check: left and right must share the same immediate + * parent with adjacent downlinks. + */ + if (scankey != NULL && + + _bt_pages_share_parent(rel, left_blkno, right_blkno, scankey, NULL)) + { + pfree(scankey); + relation_close(rel, AccessShareLock); + break; /* found a valid candidate */ + } + } + + if (scankey) + pfree(scankey); + } + } + + + /* Build and return the output row */ + uargs->returned++; + memset(nulls, false, sizeof(nulls)); + j = 0; + values[j++] = Int64GetDatum((int64) left_blkno); + values[j++] = Int64GetDatum((int64) right_blkno); + values[j++] = Float8GetDatum(100.0 * left_free / BLCKSZ); + values[j++] = Float8GetDatum(100.0 * right_free / BLCKSZ); + /* free space the merged page would have */ + values[j++] = Float8GetDatum( + 100.0 * ((int64) left_free + (int64) right_free - (int64) BLCKSZ) / BLCKSZ); + + tuple = heap_form_tuple(uargs->tupd, values, nulls); + result = HeapTupleGetDatum(tuple); + SRF_RETURN_NEXT(fctx, result); +} + +static char * +index_tuple_data(IndexTuple itup) +{ + char *ptr; + int dlen; + char *dump; + char *datacstring; + + ptr = (char *) itup + IndexInfoFindDataOffset(itup->t_info); + dlen = IndexTupleSize(itup) - IndexInfoFindDataOffset(itup->t_info); + + if (BTreeTupleIsPosting(itup)) + dlen -= IndexTupleSize(itup) - BTreeTupleGetPostingOffset(itup); + else if (BTreeTupleIsPivot(itup) && BTreeTupleGetHeapTID(itup) != NULL) + dlen -= MAXALIGN(sizeof(ItemPointerData)); + + if (dlen < 0 || dlen > INDEX_SIZE_MASK) + elog(ERROR, "invalid tuple length %d", dlen); + + dump = palloc0(dlen * 3 + 1); + datacstring = dump; + for (int off = 0; off < dlen; off++) + { + if (off > 0) + *dump++ = ' '; + sprintf(dump, "%02x", *(ptr + off) & 0xff); + dump += 2; + } + + return datacstring; +} + +/*------------------------------------------------------- + * bt_merge_detail() + * + * Given a LEFT leaf block number, find its Right sibling and Parent page + * using a proper O(log N) B-tree descent, then return one row of structural + * details for each of the three pages in this order: Parent, Left, Right. + * + * Each row contains the page header fields and a tid[] array. For leaf + * pages the array holds heap TIDs (posting-list entries are expanded). + * For the parent page the array holds the child downlinks encoded as TIDs. + * + * merge_id is reserved for future use and is always NULL. + * + * Usage: SELECT * FROM bt_merge_detail('t1_pkey', 5, false); + *------------------------------------------------------- + */ +Datum +bt_merge_detail(PG_FUNCTION_ARGS) +{ + FuncCallContext *fctx; + ua_merge_detail *uargs; + + if (SRF_IS_FIRSTCALL()) + { + text *relname = PG_GETARG_TEXT_PP(0); + int64 left_blkno_arg = PG_GETARG_INT64(1); + MemoryContext mctx; + TupleDesc tupleDesc; + Relation rel; + List *relname_list; + RangeVar *relrv; + Buffer leftbuf; + Page leftpage; + BTPageOpaque leftopaque; + OffsetNumber first_data_off; + IndexTuple first_itup; + BTScanInsert scankey; + Buffer found_buf = InvalidBuffer; + BTStack stack; + BlockNumber parent_blkno = InvalidBlockNumber; + BlockNumber left_blkno; + BlockNumber right_blkno; + + fctx = SRF_FIRSTCALL_INIT(); + mctx = MemoryContextSwitchTo(fctx->multi_call_memory_ctx); + + uargs = palloc_object(ua_merge_detail); + + if (!superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be superuser to use pageinspect functions"))); + + /* Open and validate the relation */ + relname_list = textToQualifiedNameList(relname); + relrv = makeRangeVarFromNameList(relname_list); + rel = relation_openrv(relrv, AccessShareLock); + + /* Validate that it is a B-Tree and the block number is within bounds */ + bt_index_block_validate(rel, left_blkno_arg); + + left_blkno = (BlockNumber) left_blkno_arg; + + /* + * Step 1: Read the left leaf page to get right_blkno and the first + * data tuple, which we will use to build the scan key for the + * descent. + */ + leftbuf = ReadBuffer(rel, left_blkno); + LockBuffer(leftbuf, BUFFER_LOCK_SHARE); + leftpage = BufferGetPage(leftbuf); + leftopaque = BTPageGetOpaque(leftpage); + + if (!P_ISLEAF(leftopaque)) + ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("block %u is not a leaf page", left_blkno))); + + if (P_ISDELETED(leftopaque) || P_ISHALFDEAD(leftopaque)) + ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("block %u is a deleted or half-dead page and cannot be inspected for merge", left_blkno))); + + if (P_ISMERGEDAWAY(leftopaque)) + ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("block %u is a merged-away tombstone and cannot be inspected for merge", left_blkno))); + + right_blkno = leftopaque->btpo_next; + + if (P_RIGHTMOST(leftopaque)) + ereport( + ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("block %u is the rightmost leaf it has no right sibling", + left_blkno))); + + /* + * P_FIRSTDATAKEY returns the offset of the first real data tuple, + * skipping the high-key slot on non-leftmost pages. + */ + first_data_off = P_FIRSTDATAKEY(leftopaque); + if (first_data_off > PageGetMaxOffsetNumber(leftpage)) + ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("leaf page %u is empty cannot determine parent", + left_blkno))); + + first_itup = (IndexTuple) PageGetItem( + leftpage, PageGetItemId(leftpage, first_data_off)); + + /* + * Build a typed scan key from the tuple using the relation schema. + * _bt_mkscankey reads the TupleDesc and operator classes from rel, so + * our extension does not need to know the column data types. + */ + scankey = _bt_mkscankey(rel, first_itup); + + UnlockReleaseBuffer(leftbuf); /* release left before the descent */ + + /* + * Step 2: Descend from the root in O(log N) reads. _bt_search + * returns: - found_buf: the leaf buffer (locked); we release it + * immediately. - stack: linked list from root to parent; + * stack->bts_blkno is the block number of the immediate parent + * (level-1 page). + */ + stack = _bt_search(rel, NULL, scankey, &found_buf, BT_READ, true); + if (stack != NULL) + parent_blkno = stack->bts_blkno; + + if (BufferIsValid(found_buf)) + UnlockReleaseBuffer(found_buf); + if (stack != NULL) + _bt_freestack(stack); + pfree(scankey); + + if (parent_blkno == InvalidBlockNumber) + ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("could not find parent page for leaf block %u", + left_blkno))); + + /* + * Step 3: Verify that Right is also a child of the same parent. + * + * Adjacent leaf siblings do NOT always share the same parent — they + * can straddle a parent-page boundary after a split. Instead of a + * second O(log N) descent, we simply scan the parent page we already + * have: if right_blkno does not appear as a downlink there, the two + * leaves have different parents and cannot be merged with a single + * parent update. + */ + { + Buffer parentbuf; + Page parentpage; + OffsetNumber off, + maxoff; + bool found_right = false; + + parentbuf = ReadBuffer(rel, parent_blkno); + LockBuffer(parentbuf, BUFFER_LOCK_SHARE); + parentpage = BufferGetPage(parentbuf); + maxoff = PageGetMaxOffsetNumber(parentpage); + + /* + * For non-rightmost pages, offset 1 is the HIGH KEY — its t_tid + * holds the separator key's downlink to the adjacent page, NOT a + * child of this page. Start from offset 2 to scan only real + * downlinks. + */ + { + BTPageOpaque pOpaque = BTPageGetOpaque(parentpage); + OffsetNumber scan_start = P_RIGHTMOST(pOpaque) + ? FirstOffsetNumber + : OffsetNumberNext(P_HIKEY); + + for (off = scan_start; off <= maxoff; off++) + { + IndexTuple itup = (IndexTuple) PageGetItem( + parentpage, PageGetItemId(parentpage, off)); + + if (ItemPointerGetBlockNumberNoCheck(&itup->t_tid) == right_blkno) + { + found_right = true; + break; + } + } + } + + UnlockReleaseBuffer(parentbuf); + + if (!found_right) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("pages cannot be merged: left block %u and right block " + "%u span a parent boundary", + left_blkno, right_blkno), + errhint("Parent block %u does not contain the right block.", + parent_blkno))); + } + } + + /* + * Store only the relation Oid so we can safely reopen it on every + * per-call invocation without leaking a relcache reference on LIMIT. + */ + uargs->relid = RelationGetRelid(rel); + relation_close(rel, AccessShareLock); + + uargs->parent_blkno = parent_blkno; + uargs->left_blkno = left_blkno; + uargs->right_blkno = right_blkno; + uargs->call_cntr = 0; + uargs->show_tids = PG_GETARG_BOOL(2); + + if (get_call_result_type(fcinfo, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + tupleDesc = BlessTupleDesc(tupleDesc); + + uargs->tupd = tupleDesc; + fctx->user_fctx = uargs; + + MemoryContextSwitchTo(mctx); + } + + fctx = SRF_PERCALL_SETUP(); + uargs = fctx->user_fctx; + + /* We return exactly 3 rows: 0=parent, 1=left, 2=right */ + if (uargs->call_cntr >= 3) + SRF_RETURN_DONE(fctx); + + { + Relation rel = relation_open(uargs->relid, AccessShareLock); + BlockNumber blkno; + BlockNumber btpo_prev_val; + BlockNumber btpo_next_val; + uint32 btpo_flags_val; + uint32 btpo_level_val; + Size free_size_val; + const char *role; + Buffer buf; + Page page; + BTPageOpaque opaque; + OffsetNumber off, + maxoff, + first_off; + char *high_key_hex = NULL; + char *first_val_hex = NULL; + char *last_val_hex = NULL; + int max_tids; + ItemPointerData *tids_buf; + int ntids = 0; + Datum *tids_datum; + Datum values[14]; + bool nulls[14]; + int j; + HeapTuple tuple; + Datum result; + + switch (uargs->call_cntr) + { + case 0: + blkno = uargs->parent_blkno; + role = "parent"; + break; + case 1: + blkno = uargs->left_blkno; + role = "left"; + break; + default: + blkno = uargs->right_blkno; + role = "right"; + break; + } + + buf = ReadBuffer(rel, blkno); + LockBuffer(buf, BUFFER_LOCK_SHARE); + page = BufferGetPage(buf); + opaque = BTPageGetOpaque(page); + maxoff = PageGetMaxOffsetNumber(page); + + btpo_prev_val = opaque->btpo_prev; + btpo_next_val = opaque->btpo_next; + btpo_flags_val = opaque->btpo_flags; + btpo_level_val = opaque->btpo_level; + free_size_val = PageGetFreeSpace(page); + + /* + * Collect TIDs / downlinks from the page. + * + * Leaf pages: iterate data tuples (skip high key via P_FIRSTDATAKEY). + * - Posting-list tuples: expand with BTreeTupleGetPosting(). - Normal + * tuples: take t_tid directly. + * + * Parent page: all items are pivot tuples; the block-number part of + * t_tid is the child downlink. Skip minus-infinity entries that have + * no real downlink (InvalidBlockNumber). + * + * Upper bound: 2*BLCKSZ/sizeof(ItemPointerData) covers any real page. + */ + max_tids = 2 * BLCKSZ / sizeof(ItemPointerData); + tids_buf = (ItemPointerData *) palloc(max_tids * sizeof(ItemPointerData)); + + if (P_ISLEAF(opaque) && !P_ISMERGEDAWAY(opaque) && !P_ISDELETED(opaque)) + { + first_off = P_FIRSTDATAKEY(opaque); + for (off = first_off; off <= maxoff; off++) + { + IndexTuple itup = + (IndexTuple) PageGetItem(page, PageGetItemId(page, off)); + + if (BTreeTupleIsPosting(itup)) + { + int nposting = BTreeTupleGetNPosting(itup); + ItemPointer posting = BTreeTupleGetPosting(itup); + + for (int i = 0; i < nposting && ntids < max_tids; i++) + tids_buf[ntids++] = posting[i]; + } + else + { + if (ntids < max_tids) + tids_buf[ntids++] = itup->t_tid; + } + } + + /* + * High key + */ + if (!P_RIGHTMOST(opaque)) + { + IndexTuple hikey = + (IndexTuple) PageGetItem(page, PageGetItemId(page, P_HIKEY)); + + high_key_hex = index_tuple_data(hikey); + } + + /* + * First and last data tuples + */ + if (first_off <= maxoff) + { + IndexTuple first_itup = + (IndexTuple) PageGetItem(page, PageGetItemId(page, first_off)); + IndexTuple last_itup = + (IndexTuple) PageGetItem(page, PageGetItemId(page, maxoff)); + + first_val_hex = index_tuple_data(first_itup); + last_val_hex = index_tuple_data(last_itup); + } + } + else if (!P_ISMERGEDAWAY(opaque) && !P_ISDELETED(opaque)) + { + for (off = FirstOffsetNumber; off <= maxoff; off++) + { + IndexTuple itup = + (IndexTuple) PageGetItem(page, PageGetItemId(page, off)); + + if (BTreeTupleIsPivot(itup) && + ItemPointerGetBlockNumberNoCheck(&itup->t_tid) != + InvalidBlockNumber) + { + if (ntids < max_tids) + tids_buf[ntids++] = itup->t_tid; + } + } + } + + UnlockReleaseBuffer(buf); + relation_close(rel, AccessShareLock); + + /* Convert tids_buf into a Datum array for construct_array_builtin */ + tids_datum = (Datum *) palloc(ntids * sizeof(Datum)); + for (int i = 0; i < ntids; i++) + tids_datum[i] = ItemPointerGetDatum(&tids_buf[i]); + + /* Build output tuple */ + memset(nulls, false, sizeof(nulls)); + j = 0; + values[j++] = CStringGetTextDatum(role); + values[j++] = Int64GetDatum((int64) blkno); + values[j++] = Int64GetDatum((int64) btpo_prev_val); + values[j++] = Int64GetDatum((int64) btpo_next_val); + values[j++] = Int64GetDatum((int64) btpo_level_val); + values[j++] = Int32GetDatum((int32) btpo_flags_val); + values[j++] = Int32GetDatum((int32) free_size_val); + values[j++] = Float8GetDatum(100.0 * free_size_val / BLCKSZ); + values[j++] = Int32GetDatum((int32) ntids); + if (high_key_hex) + values[j++] = CStringGetTextDatum(high_key_hex); + else + nulls[j++] = true; + if (first_val_hex) + values[j++] = CStringGetTextDatum(first_val_hex); + else + nulls[j++] = true; + if (last_val_hex) + values[j++] = CStringGetTextDatum(last_val_hex); + else + nulls[j++] = true; + nulls[j++] = true; /* merge_id: RFU, always NULL */ + + if (uargs->show_tids) + values[j++] = PointerGetDatum(construct_array_builtin(tids_datum, ntids, TIDOID)); + else + nulls[j++] = true; + + tuple = heap_form_tuple(uargs->tupd, values, nulls); + result = HeapTupleGetDatum(tuple); + + uargs->call_cntr++; + SRF_RETURN_NEXT(fctx, result); + } +} + +Datum +bt_merge(PG_FUNCTION_ARGS) +{ + text *relname = PG_GETARG_TEXT_PP(0); + float8 merge_candidate_max_pct = PG_GETARG_FLOAT8(1); + float8 merge_destination_max_pct = PG_GETARG_FLOAT8(2); + int32 num_pages = PG_GETARG_INT32(3); + Relation rel; + RangeVar *relrv; + int32 merges_performed; + + + if (!superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be superuser to use pageinspect functions"))); + + relrv = makeRangeVarFromNameList(textToQualifiedNameList(relname)); + rel = relation_openrv(relrv, AccessShareLock); + + if (!IS_INDEX(rel) || !IS_BTREE(rel)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a %s index", + RelationGetRelationName(rel), "btree"))); + + /* + * Reject attempts to read non-local temporary relations; we would be + * likely to get wrong data since we have no visibility into the owning + * session's local buffers. + */ + if (RELATION_IS_OTHER_TEMP(rel)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot access temporary tables of other sessions"))); + + + merges_performed = _bt_merge_index(rel, merge_candidate_max_pct, merge_destination_max_pct, num_pages); + + relation_close(rel, AccessShareLock); + + + PG_RETURN_INT32(merges_performed); + +} diff --git a/contrib/pageinspect/pageinspect--1.12--1.13.sql b/contrib/pageinspect/pageinspect--1.12--1.13.sql index 07ec70a5de3..4ca54567d59 100644 --- a/contrib/pageinspect/pageinspect--1.12--1.13.sql +++ b/contrib/pageinspect/pageinspect--1.12--1.13.sql @@ -72,3 +72,66 @@ LANGUAGE SQL PARALLEL RESTRICTED BEGIN ATOMIC SELECT * FROM heap_page_item_attrs(page, rel_oid, false); END; + + +-- +-- bt_find_merge_candidates(relname, min_pct_threshold, num_pages) +-- +-- Scan the B-tree leaf chain and return up to num_pages LEFT block numbers +-- of adjacent pairs that are merge candidates (both pages <= min_pct_threshold +-- percent full, and combined content fits within the target fillfactor). +-- +CREATE FUNCTION bt_find_merge_candidates( + IN relname text, + IN merge_candidate_max_pct float8 DEFAULT 10.0, + IN merge_destination_max_pct float8 DEFAULT 90.0, + IN num_pages int4 DEFAULT 1, + OUT left_blkno int8, + OUT right_blkno int8, + OUT free_space_left float8, + OUT free_space_right float8, + OUT result_free_space float8) +RETURNS SETOF record +AS 'MODULE_PATHNAME', 'bt_find_merge_candidates' +LANGUAGE C PARALLEL SAFE; + +-- +-- bt_merge_detail(relname, left_blkno) +-- +-- Given the LEFT leaf block number, find its Right sibling and Parent via +-- a proper B-tree descent (O(log N)), then return one detail row for each +-- of the three pages: Parent, Left, Right. +-- +CREATE FUNCTION bt_merge_detail( + IN relname text, + IN left_blkno int8, + IN show_tids boolean, + OUT page_role text, + OUT blkno int8, + OUT btpo_prev int8, + OUT btpo_next int8, + OUT btpo_level int8, + OUT btpo_flags int4, + OUT free_size int4, + OUT pct_free float8, + OUT num_tids int4, + OUT high_key text, + OUT first_val text, + OUT last_val text, + OUT merge_id int4, + OUT tids tid[] +) +RETURNS SETOF record +AS 'MODULE_PATHNAME', 'bt_merge_detail' +LANGUAGE C PARALLEL SAFE; + +CREATE FUNCTION bt_merge( + IN relname text, + IN merge_candidate_max_pct float8 DEFAULT 10.0, + IN merge_destination_max_pct float8 DEFAULT 90.0, + IN num_pages int4 DEFAULT 1, + OUT merges_performed int4 +) +RETURNS int4 +AS 'MODULE_PATHNAME', 'bt_merge' +LANGUAGE C PARALLEL SAFE; \ No newline at end of file -- 2.43.0