On 7/24/25 16:40, Peter Geoghegan wrote:
> On Thu, Jul 24, 2025 at 7:19 AM Tomas Vondra <to...@vondra.me> wrote:
>> I got a bit bored yesterday, so I gave this a try and whipped up a patch
>> that adds two pgstattuple functins that I think could be useful for
>> analyzing index metrics that matter for prefetching.
> 
> This seems quite useful.
> 
> I notice that you're not accounting for posting lists. That'll lead to
> miscounts of the number of heap blocks in many cases. I think that
> that's worth fixing, even given that this patch is experimental.
> 

Yeah, I forgot about that. Should be fixed in the v2. Admittedly I don't
know that much about nbtree internals, so this is mostly copy pasting
from verify_nbtree.

>> It's trivial to summarize this into a per-index statistic (of course,
>> there may be some inaccuracies when the run spans multiple ranges), but
>> it also seems useful to be able to look at parts of the index.
> 
> FWIW in my experience, the per-leaf-page "nhtids:nhblks" tends to be
> fairly consistent across all leaf pages from a given index. There are
> no doubt some exceptions, but they're probably pretty rare.
> 

Yeah, probably. And we'll probably test on such uniform data sets, or at
least we we'll start with those. But at some point I'd like to test with
some of these "weird" indexes too, if only to test how well the prefetch
heuristics adjusts the distance.

>> Second, the index is walked sequentially in physical order, from block 0
>> to the last block. But that's not really what the index prefetch sees.
>> To make it "more accurate" it'd be better to just scan the leaf pages as
>> if during a "full index scan".
> 
> Why not just do it that way to begin with? It wouldn't be complicated
> to make the function follow a chain of right sibling links.
> 

I have a very good reason why I didn't do it that way. I was lazy. But
v2 should be doing that, I think.

> I suggest an interface that takes a block number, and an nblocks int8
> argument that must be >= 1. The function would start from the block
> number arg leaf page. If it's not a non-ignorable leaf page, throw an
> error. Otherwise, count the number of distinct heap blocks on the leaf
> page, and count the number of heap blocks on each additional leaf page
> to the right -- until we've counted the heap blocks from nblocks-many
> leaf pages (or until we reach the rightmost leaf page).
> 

Yeah, this interface seems useful. I suppose it'll be handy when looking
at an index scan, to get stats from the currently loaded batches. In
principle you get that from v3 by filtering, but it might be slow on
large indexes. I'll try doing that in v3.

> I suggest that a P_IGNORE() page shouldn't have its heap blocks
> counted, and shouldn't count towards our nblocks tally of leaf pages
> whose heap blocks are to be counted. Upon encountering a P_IGNORE()
> page, just move to the right without doing anything. Note that the
> rightmost page cannot be P_IGNORE().
> 

I think v2 does all of this.

> This scheme will always succeed, no matter the nblocks argument,
> provided the initial leaf page is a valid leaf page (and provided the
> nblocks arg is >= 1).
> 
> I get that this is just a prototype that might not go anywhere, but
> the scheme I've described requires few changes.
> 

Yep, thanks.


-- 
Tomas Vondra
From 09576e519adfe3b8db47de3b4b03f1f9374e3ecf Mon Sep 17 00:00:00 2001
From: Tomas Vondra <to...@vondra.me>
Date: Thu, 24 Jul 2025 13:00:24 +0200
Subject: [PATCH v2] pgstattuple: analyze TIDs on btree leaf pages

The patch adds two functions, that are meant to provide data for
additional analysis rather than computing something final. Each function
splits the index into a sequence of block ranges (of given length), and
calculates some metrics on that.

pgstatindex_nheap
  - number of leafs in the range
  - number of block numbers
  - number of distinct block numbers
  - number of runs (of the same block)

pgstatindex_runs
  - number of leafs in the range
  - run length
  - number of runs with the length

It's trivial to summarize this into a per-index statistic (of course,
there may be some inaccuracies when the run spans multiple ranges), but
it also seems useful to be able to look at parts of the index.

This is meant as a quick experimental patch, to help with generating
better datasets for the evaluation. And I think it works for that, and I
don't have immediate plans to work on this outside that context.
---
 contrib/pgstattuple/Makefile                  |   3 +-
 contrib/pgstattuple/pgstatindex.c             | 481 ++++++++++++++++++
 contrib/pgstattuple/pgstattuple--1.5--1.6.sql |  31 ++
 contrib/pgstattuple/pgstattuple.control       |   2 +-
 4 files changed, 515 insertions(+), 2 deletions(-)
 create mode 100644 contrib/pgstattuple/pgstattuple--1.5--1.6.sql

diff --git a/contrib/pgstattuple/Makefile b/contrib/pgstattuple/Makefile
index c5b17fc703e..d5c62ba36f9 100644
--- a/contrib/pgstattuple/Makefile
+++ b/contrib/pgstattuple/Makefile
@@ -10,7 +10,8 @@ OBJS = \
 EXTENSION = pgstattuple
 DATA = pgstattuple--1.4.sql pgstattuple--1.4--1.5.sql \
 	pgstattuple--1.3--1.4.sql pgstattuple--1.2--1.3.sql \
-	pgstattuple--1.1--1.2.sql pgstattuple--1.0--1.1.sql
+	pgstattuple--1.1--1.2.sql pgstattuple--1.0--1.1.sql \
+	pgstattuple--1.5--1.6.sql
 PGFILEDESC = "pgstattuple - tuple-level statistics"
 
 REGRESS = pgstattuple
diff --git a/contrib/pgstattuple/pgstatindex.c b/contrib/pgstattuple/pgstatindex.c
index 4b9d76ec4e4..d88b47595ac 100644
--- a/contrib/pgstattuple/pgstatindex.c
+++ b/contrib/pgstattuple/pgstatindex.c
@@ -62,6 +62,9 @@ PG_FUNCTION_INFO_V1(pg_relpages_v1_5);
 PG_FUNCTION_INFO_V1(pg_relpagesbyid_v1_5);
 PG_FUNCTION_INFO_V1(pgstatginindex_v1_5);
 
+PG_FUNCTION_INFO_V1(pgstatindex_nheap_v1_6);
+PG_FUNCTION_INFO_V1(pgstatindex_runs_v1_6);
+
 Datum		pgstatginindex_internal(Oid relid, FunctionCallInfo fcinfo);
 
 #define IS_INDEX(r) ((r)->rd_rel->relkind == RELKIND_INDEX)
@@ -128,6 +131,9 @@ static Datum pgstatindex_impl(Relation rel, FunctionCallInfo fcinfo);
 static int64 pg_relpages_impl(Relation rel);
 static void GetHashPageStats(Page page, HashIndexStat *stats);
 
+static Datum pgstatindex_nheap_impl(Relation rel, int64 nblocks, FunctionCallInfo fcinfo);
+static Datum pgstatindex_runs_impl(Relation rel, int64 nblocks, FunctionCallInfo fcinfo);
+
 /* ------------------------------------------------------
  * pgstatindex()
  *
@@ -756,3 +762,478 @@ GetHashPageStats(Page page, HashIndexStat *stats)
 	}
 	stats->free_space += PageGetExactFreeSpace(page);
 }
+
+/*
+ */
+Datum
+pgstatindex_nheap_v1_6(PG_FUNCTION_ARGS)
+{
+	text	   *relname = PG_GETARG_TEXT_PP(0);
+	int64		nblocks = PG_GETARG_INT64(1);
+	Relation	rel;
+	RangeVar   *relrv;
+
+	relrv = makeRangeVarFromNameList(textToQualifiedNameList(relname));
+	rel = relation_openrv(relrv, AccessShareLock);
+
+	PG_RETURN_DATUM(pgstatindex_nheap_impl(rel, nblocks, fcinfo));
+}
+
+/*
+ */
+Datum
+pgstatindex_runs_v1_6(PG_FUNCTION_ARGS)
+{
+	text	   *relname = PG_GETARG_TEXT_PP(0);
+	int64		nblocks = PG_GETARG_INT64(1);
+	Relation	rel;
+	RangeVar   *relrv;
+
+	relrv = makeRangeVarFromNameList(textToQualifiedNameList(relname));
+	rel = relation_openrv(relrv, AccessShareLock);
+
+	PG_RETURN_DATUM(pgstatindex_runs_impl(rel, nblocks, fcinfo));
+}
+
+typedef struct BlockBuffer
+{
+	int			nitems;
+	int			maxitems;
+	BlockNumber *items;
+} BlockBuffer;
+
+static int
+blocknum_cmp(const void *a, const void *b)
+{
+	return memcmp(a, b, sizeof(BlockNumber));
+}
+
+static int
+count_block_runs(BlockBuffer blocks)
+{
+	int			nruns = 1;
+
+	for (int i = 1; i < blocks.nitems; i++)
+	{
+		if (blocks.items[i] != blocks.items[i-1])
+			nruns++;
+	}
+
+	return nruns;
+}
+
+static int
+count_blocks_distinct(BlockBuffer blocks)
+{
+	pg_qsort(blocks.items, blocks.nitems, sizeof(BlockNumber), blocknum_cmp);
+
+	return count_block_runs(blocks);
+}
+
+static void
+buffer_init(BlockBuffer *blocks)
+{
+	blocks->nitems = 0;
+	blocks->maxitems = 0;
+	blocks->items = NULL;
+}
+
+static void
+buffer_add_block(BlockBuffer *blocks, BlockNumber block)
+{
+	if (blocks->nitems == blocks->maxitems)
+	{
+		if (blocks->maxitems == 0)
+		{
+			blocks->maxitems = 64;
+			blocks->items = palloc_array(BlockNumber, blocks->maxitems);
+		}
+		else
+		{
+			blocks->maxitems *= 2;
+			blocks->items = repalloc_array(blocks->items, BlockNumber,
+										   blocks->maxitems);
+		}
+	}
+
+	blocks->items[blocks->nitems++] = block;
+}
+
+static Datum
+pgstatindex_nheap_impl(Relation rel, int64 range_len, FunctionCallInfo fcinfo)
+{
+	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+
+	TupleDesc	tupdesc;
+	Tuplestorestate *tupstore;
+
+	Datum		values[5];
+	bool		nulls[5];
+
+	BlockBuffer	blocks;	/* buffer with blocks from the range */
+	BlockNumber	blkno;	/* block number of the (first) leaf in the range */
+	int64		nblocks;
+	Buffer		buf;
+	int64		seq = 0;
+
+	/* no NULLs */
+	memset(nulls, 0, sizeof(nulls));
+
+	if (range_len < 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("range length has to be at least 1")));
+
+	if (!IS_INDEX(rel) || !IS_BTREE(rel))
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("relation \"%s\" is not a btree index",
+						RelationGetRelationName(rel))));
+
+	/*
+	 * 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")));
+
+	/*
+	 * A !indisready index could lead to ERRCODE_DATA_CORRUPTED later, so exit
+	 * early.  We're capable of assessing an indisready&&!indisvalid index,
+	 * but the results could be confusing.  For example, the index's size
+	 * could be too low for a valid index of the table.
+	 */
+	if (!rel->rd_index->indisvalid)
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("index \"%s\" is not valid",
+						RelationGetRelationName(rel))));
+
+	InitMaterializedSRF(fcinfo, 0);
+	tupdesc = rsinfo->setDesc;
+	tupstore = rsinfo->setResult;
+
+	/* pre-allocate space for the maximum number of TIDs we might see
+	 * on range_len pages */
+	buffer_init(&blocks);
+
+	/* find the left-most leaf page */
+	buf = _bt_get_endpoint(rel, 0, false);
+	blkno = InvalidBlockNumber;
+	nblocks = 0;	/* just a counter of blocks we've seen so far */
+
+	while (BufferIsValid(buf))
+	{
+		Page		page;
+		BTPageOpaque opaque;
+
+		/* remember first block of the new range */
+		if (!BlockNumberIsValid(blkno))
+			blkno = BufferGetBlockNumber(buf);
+
+		CHECK_FOR_INTERRUPTS();
+
+		/* Read and lock buffer */
+		page = BufferGetPage(buf);
+		opaque = BTPageGetOpaque(page);
+
+		/*
+		 * Ignore deleted/dead pages, and internal (non-leaf) pages.
+		 */
+		if (!P_ISDELETED(opaque) && !P_IGNORE(opaque) && P_ISLEAF(opaque))
+		{
+			OffsetNumber	offset = FirstOffsetNumber;
+			OffsetNumber	maxoffset = PageGetMaxOffsetNumber(page);
+			bool			rightmost = P_RIGHTMOST(opaque);
+
+			while (offset < maxoffset)
+			{
+				ItemId		id = PageGetItemId(page, offset);
+				IndexTuple	itup = (IndexTuple) PageGetItem(page, id);
+				bool		ispivot = (!rightmost && offset == P_HIKEY);
+
+				offset++;
+
+				/* ignore pivot tuples */
+				if (ispivot)
+					continue;
+
+				if (BTreeTupleIsPosting(itup))
+				{
+					for (int i = 0; i < BTreeTupleGetNPosting(itup); i++)
+					{
+						ItemPointer tid = BTreeTupleGetPostingN(itup, i);
+						BlockNumber	block = ItemPointerGetBlockNumber(tid);
+
+						buffer_add_block(&blocks, block);
+					}
+				}
+				else
+				{
+					BlockNumber	block = ItemPointerGetBlockNumber(&itup->t_tid);
+
+					buffer_add_block(&blocks, block);
+				}
+			}
+
+			/* we've added a block to the range */
+			nblocks++;
+		}
+
+		/*
+		 * If this was the last block in the range, or if this is the last
+		 * leaf page in general, generate the tuple. Count the distinct
+		 * blocks and add a tuple into the result set (if there are any TIDs).
+		 */
+		if ((nblocks == range_len) || (opaque->btpo_next == P_NONE))
+		{
+			if (blocks.nitems > 0)
+			{
+				HeapTuple	tuple;
+				int			ndistinct;
+				int			nruns;
+
+				nruns = count_block_runs(blocks);
+
+				/* this modifies the array, so do once */
+				ndistinct = count_blocks_distinct(blocks);
+
+				values[0] = Int64GetDatum(++seq);
+				values[1] = Int64GetDatum(blkno);
+				values[2] = Int64GetDatum(blocks.nitems);
+				values[3] = Int64GetDatum(ndistinct);
+				values[4] = Int64GetDatum(nruns);
+
+				/* Build and return the result tuple */
+				tuple = heap_form_tuple(tupdesc, values, nulls);
+
+				tuplestore_puttuple(tupstore, tuple);
+
+				blocks.nitems = 0;
+
+				/* reset the block, will be set at the new loop */
+				blkno = InvalidBlockNumber;
+				nblocks = 0;
+			}
+		}
+
+		/* no more leafs, we're done */
+		if (opaque->btpo_next == P_NONE)
+		{
+			_bt_relbuf(rel, buf);
+			break;
+		}
+
+		/* step right one page */
+		buf = _bt_relandgetbuf(rel, buf, opaque->btpo_next, BT_READ);
+	}
+
+	relation_close(rel, AccessShareLock);
+
+	PG_RETURN_NULL();
+}
+
+static void
+count_run_lengths(BlockBuffer blocks, int *lengths)
+{
+	int			len = 1;
+	BlockNumber	curr = blocks.items[0];
+
+	for (int i = 0; i < blocks.nitems; i++)
+	{
+		if (blocks.items[i] != curr)
+		{
+			lengths[len]++;
+			len = 1;
+			curr = blocks.items[i];
+			continue;
+		}
+
+		len++;
+	}
+
+	lengths[len]++;
+}
+
+static Datum
+pgstatindex_runs_impl(Relation rel, int64 range_len, FunctionCallInfo fcinfo)
+{
+	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+
+	int			*run_lengths = NULL;
+	TupleDesc	tupdesc;
+	Tuplestorestate *tupstore;
+
+	Datum		values[4];
+	bool		nulls[4];
+
+	BlockBuffer	blocks;	/* buffer with blocks from the range */
+	BlockNumber	blkno;	/* block number of the (first) leaf in the range */
+	int64		nblocks;
+	Buffer		buf;
+	int64		seq = 0;
+
+	/* no NULLs */
+	memset(nulls, 0, sizeof(nulls));
+
+	if (range_len < 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("range length has to be at least 1")));
+
+	if (!IS_INDEX(rel) || !IS_BTREE(rel))
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("relation \"%s\" is not a btree index",
+						RelationGetRelationName(rel))));
+
+	/*
+	 * 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")));
+
+	/*
+	 * A !indisready index could lead to ERRCODE_DATA_CORRUPTED later, so exit
+	 * early.  We're capable of assessing an indisready&&!indisvalid index,
+	 * but the results could be confusing.  For example, the index's size
+	 * could be too low for a valid index of the table.
+	 */
+	if (!rel->rd_index->indisvalid)
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("index \"%s\" is not valid",
+						RelationGetRelationName(rel))));
+
+	InitMaterializedSRF(fcinfo, 0);
+	tupdesc = rsinfo->setDesc;
+	tupstore = rsinfo->setResult;
+
+	/* pre-allocate space for the maximum number of TIDs we might see
+	 * on range_len pages */
+	buffer_init(&blocks);
+
+	/* lengths of runs (the range could be one long run) */
+	run_lengths = palloc_array(int, (range_len * MaxTIDsPerBTreePage + 1));
+
+	/* find the left-most leaf page */
+	buf = _bt_get_endpoint(rel, 0, false);
+	blkno = InvalidBlockNumber;
+	nblocks = 0;	/* just a counter of blocks we've seen so far */
+
+	while (BufferIsValid(buf))
+	{
+		Page		page;
+		BTPageOpaque opaque;
+
+		if (!BlockNumberIsValid(blkno))
+			blkno = BufferGetBlockNumber(buf);
+
+		CHECK_FOR_INTERRUPTS();
+
+		page = BufferGetPage(buf);
+		opaque = BTPageGetOpaque(page);
+
+		/*
+		 * Ignore deleted/dead pages, and internal (non-leaf) pages.
+		 */
+		if (!P_ISDELETED(opaque) && !P_IGNORE(opaque) && P_ISLEAF(opaque))
+		{
+			OffsetNumber	offset = FirstOffsetNumber;
+			OffsetNumber	maxoffset = PageGetMaxOffsetNumber(page);
+			bool			rightmost = P_RIGHTMOST(opaque);
+
+			while (offset < maxoffset)
+			{
+				ItemId		id = PageGetItemId(page, offset);
+				IndexTuple	itup = (IndexTuple) PageGetItem(page, id);
+				bool		ispivot = (!rightmost && offset == P_HIKEY);
+
+				offset++;
+
+				/* ignore pivot tuples */
+				if (ispivot)
+					continue;
+
+				if (BTreeTupleIsPosting(itup))
+				{
+					for (int i = 0; i < BTreeTupleGetNPosting(itup); i++)
+					{
+						ItemPointer tid = BTreeTupleGetPostingN(itup, i);
+						BlockNumber	block = ItemPointerGetBlockNumber(tid);
+
+						buffer_add_block(&blocks, block);
+					}
+				}
+				else
+				{
+					BlockNumber	block = ItemPointerGetBlockNumber(&itup->t_tid);
+
+					buffer_add_block(&blocks, block);
+				}
+			}
+
+			/* we've added block to the range */
+			nblocks++;
+		}
+
+		/*
+		 * If this was the last block in the range, or if this is the last
+		 * leaf page in general, generate the tuple. Count the distinct
+		 * blocks and add a tuple into the result set (if there are any TIDs).
+		 */
+		if ((nblocks == range_len) || (opaque->btpo_next == P_NONE))
+
+		{
+			if (blocks.nitems > 0)
+			{
+				HeapTuple	tuple;
+
+				memset(run_lengths, 0, sizeof(int) * (range_len * MaxTIDsPerBTreePage + 1));
+				count_run_lengths(blocks, run_lengths);
+
+				for (int i = 1; i <= (range_len * MaxTIDsPerBTreePage); i++)
+				{
+					if (run_lengths[i] > 0)
+					{
+						values[0] = Int64GetDatum(++seq);
+						values[1] = Int64GetDatum(blkno);
+						values[2] = Int32GetDatum(i);
+						values[3] = Int32GetDatum(run_lengths[i]);
+
+						/* Build and return the result tuple */
+						tuple = heap_form_tuple(tupdesc, values, nulls);
+
+						tuplestore_puttuple(tupstore, tuple);
+					}
+				}
+
+				blocks.nitems = 0;
+
+				/* reset the block, will be set at the new loop */
+				blkno = InvalidBlockNumber;
+				nblocks = 0;
+			}
+		}
+
+		if (opaque->btpo_next == P_NONE)
+		{
+			_bt_relbuf(rel, buf);
+			break;
+		}
+
+		/* step right one page */
+		buf = _bt_relandgetbuf(rel, buf, opaque->btpo_next, BT_READ);
+	}
+
+	relation_close(rel, AccessShareLock);
+
+	PG_RETURN_NULL();
+}
diff --git a/contrib/pgstattuple/pgstattuple--1.5--1.6.sql b/contrib/pgstattuple/pgstattuple--1.5--1.6.sql
new file mode 100644
index 00000000000..3841c71606a
--- /dev/null
+++ b/contrib/pgstattuple/pgstattuple--1.5--1.6.sql
@@ -0,0 +1,31 @@
+/* contrib/pgstattuple/pgstattuple--1.5--1.6.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pgstattuple UPDATE TO '1.6'" to load this file. \quit
+
+CREATE OR REPLACE FUNCTION pgstatindex_nheap(IN relname text,
+    IN blocks BIGINT,			-- length of ranges to analyze
+    OUT seq BIGINT,				-- sequential ID of tuple/range
+    OUT block BIGINT,			-- first block of a range
+    OUT num_items BIGINT,		-- number of heap TIDs
+    OUT num_blocks BIGINT,		-- number of distinct blocks
+    OUT num_runs BIGINT)		-- number of continuous runs
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pgstatindex_nheap_v1_6'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+REVOKE EXECUTE ON FUNCTION pgstatindex_nheap(text, bigint) FROM PUBLIC;
+GRANT EXECUTE ON FUNCTION pgstatindex_nheap(text, bigint) TO pg_stat_scan_tables;
+
+CREATE OR REPLACE FUNCTION pgstatindex_runs(IN relname text,
+    IN blocks BIGINT,			-- length of ranges to analyze
+    OUT seq BIGINT,				-- sequential ID of tuple/range
+    OUT block BIGINT,			-- first block of a range
+    OUT run_length INT,			-- number of leaf pages
+    OUT run_count INT)		-- number of heap TIDs
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pgstatindex_runs_v1_6'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+REVOKE EXECUTE ON FUNCTION pgstatindex_runs(text, bigint) FROM PUBLIC;
+GRANT EXECUTE ON FUNCTION pgstatindex_runs(text, bigint) TO pg_stat_scan_tables;
diff --git a/contrib/pgstattuple/pgstattuple.control b/contrib/pgstattuple/pgstattuple.control
index 6af40757b27..80d06958e90 100644
--- a/contrib/pgstattuple/pgstattuple.control
+++ b/contrib/pgstattuple/pgstattuple.control
@@ -1,5 +1,5 @@
 # pgstattuple extension
 comment = 'show tuple-level statistics'
-default_version = '1.5'
+default_version = '1.6'
 module_pathname = '$libdir/pgstattuple'
 relocatable = true
-- 
2.50.1

Reply via email to